from datetime import date
from decimal import Decimal

from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import RequestFactory
from django.test import TestCase
from django.urls import reverse

from app.models.classes import AcademicClass, Class, Stream, Term
from app.models.school_settings import AcademicYear, SchoolSetting, Section
from app.models.staffs import Staff
from app.models.students import Student
from app.models.subjects import Subject
from app.services.dashboard.overview import get_overview_context
from app.services.level_scope import (
    get_level_classes_queryset,
    get_level_students_queryset,
    get_level_subjects_queryset,
)
from secondary.models import (
    ALevelCompetencyScale,
    ALevelComponentWeight,
    ALevelModuleAssessment,
    ALevelSubjectModule,
    ContinuousAssessmentRecord,
    ContinuousAssessmentTask,
    SecondaryComputationPolicy,
    SecondaryGradeBand,
    UNEBSubmissionBatch,
)
from secondary.services.cbc import (
    compute_subject_result,
    lock_uneb_batch,
    upsert_uneb_submission_item,
)
from secondary.services.alevel import compute_a_level_subject_result
from secondary.views import _secondary_sections_queryset


class SecondarySectionQueryTests(TestCase):
    def test_olevel_and_alevel_sections_are_scoped_by_active_level(self):
        o_level = Section.objects.create(section_name="O-Level")
        o_level_alias = Section.objects.create(section_name="O level")
        a_level = Section.objects.create(section_name="A-Level")
        Section.objects.create(section_name="Primary")

        lower_sections = _secondary_sections_queryset(
            active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER
        )
        upper_sections = _secondary_sections_queryset(
            active_level=SchoolSetting.EducationLevel.SECONDARY_UPPER
        )

        self.assertSetEqual(
            set(lower_sections.values_list("id", flat=True)),
            {o_level.id, o_level_alias.id},
        )
        self.assertSetEqual(
            set(upper_sections.values_list("id", flat=True)),
            {a_level.id},
        )

    def test_generic_secondary_section_is_used_when_specific_aliases_are_missing(self):
        secondary = Section.objects.create(section_name="Secondary")
        Section.objects.create(section_name="Primary")

        lower_sections = _secondary_sections_queryset(
            active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER
        )
        upper_sections = _secondary_sections_queryset(
            active_level=SchoolSetting.EducationLevel.SECONDARY_UPPER
        )

        self.assertSetEqual(set(lower_sections.values_list("id", flat=True)), {secondary.id})
        self.assertSetEqual(set(upper_sections.values_list("id", flat=True)), {secondary.id})

    def test_level_specific_filter_does_not_fallback_to_opposite_secondary_section(self):
        Section.objects.create(section_name="A-Level")

        lower_sections = _secondary_sections_queryset(
            active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER
        )

        self.assertFalse(lower_sections.exists())


class LevelScopeServiceTests(TestCase):
    def setUp(self):
        self.primary_section = Section.objects.create(section_name="Primary")
        self.o_level_section = Section.objects.create(section_name="O-Level")
        self.a_level_section = Section.objects.create(section_name="A-Level")
        self.secondary_generic = Section.objects.create(section_name="Secondary")

        self.year = AcademicYear.objects.create(academic_year="2026", is_current=True)
        self.term = Term.objects.create(
            academic_year=self.year,
            term="1",
            start_date=date(2026, 2, 2),
            end_date=date(2026, 4, 30),
            is_current=True,
        )
        self.stream = Stream.objects.create(stream="Blue")

    def _create_student(self, *, name, class_obj):
        return Student.objects.create(
            reg_no="",
            student_name=name,
            gender="F",
            birthdate=date(2011, 1, 1),
            nationality="Ugandan",
            religion="Catholic",
            address="Kampala",
            guardian="Guardian",
            relationship="Parent",
            contact="0700000000",
            academic_year=self.year,
            current_class=class_obj,
            stream=self.stream,
            term=self.term,
            is_active=True,
        )

    def test_secondary_lower_scope_excludes_primary_data(self):
        primary_class = Class.objects.create(name="Primary 4", code="P4", section=self.primary_section)
        lower_class = Class.objects.create(name="Senior 2", code="S2", section=self.o_level_section)
        upper_class = Class.objects.create(name="Senior 5", code="S5", section=self.a_level_section)

        Subject.objects.create(code="ENG-P", name="English P", credit_hours=4, section=self.primary_section, type="Core")
        lower_subject = Subject.objects.create(
            code="ENG-S2",
            name="English S2",
            credit_hours=4,
            section=self.o_level_section,
            type="Core",
        )
        Subject.objects.create(code="ENG-S5", name="English S5", credit_hours=4, section=self.a_level_section, type="Core")

        self._create_student(name="Primary Student", class_obj=primary_class)
        lower_student = self._create_student(name="Lower Student", class_obj=lower_class)
        self._create_student(name="Upper Student", class_obj=upper_class)

        lower_classes = get_level_classes_queryset(active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER)
        lower_subjects = get_level_subjects_queryset(active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER)
        lower_students = get_level_students_queryset(active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER)

        self.assertSetEqual(set(lower_classes.values_list("id", flat=True)), {lower_class.id})
        self.assertSetEqual(set(lower_subjects.values_list("id", flat=True)), {lower_subject.id})
        self.assertSetEqual(set(lower_students.values_list("id", flat=True)), {lower_student.id})

    def test_generic_secondary_section_uses_class_level_split(self):
        s2_class = Class.objects.create(name="Senior 2", code="S2", section=self.secondary_generic)
        s5_class = Class.objects.create(name="Senior 5", code="S5", section=self.secondary_generic)

        self._create_student(name="S2 Student", class_obj=s2_class)
        self._create_student(name="S5 Student", class_obj=s5_class)

        lower_classes = get_level_classes_queryset(active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER)
        upper_classes = get_level_classes_queryset(active_level=SchoolSetting.EducationLevel.SECONDARY_UPPER)
        lower_students = get_level_students_queryset(active_level=SchoolSetting.EducationLevel.SECONDARY_LOWER)
        upper_students = get_level_students_queryset(active_level=SchoolSetting.EducationLevel.SECONDARY_UPPER)

        self.assertSetEqual(set(lower_classes.values_list("id", flat=True)), {s2_class.id})
        self.assertSetEqual(set(upper_classes.values_list("id", flat=True)), {s5_class.id})
        self.assertSetEqual(set(lower_students.values_list("current_class_id", flat=True)), {s2_class.id})
        self.assertSetEqual(set(upper_students.values_list("current_class_id", flat=True)), {s5_class.id})


class DashboardModeScopeTests(TestCase):
    def setUp(self):
        self.factory = RequestFactory()
        self.primary_section = Section.objects.create(section_name="Primary")
        self.secondary_section = Section.objects.create(section_name="O-Level")
        self.year = AcademicYear.objects.create(academic_year="2026", is_current=True)
        self.term = Term.objects.create(
            academic_year=self.year,
            term="1",
            start_date=date(2026, 2, 2),
            end_date=date(2026, 4, 30),
            is_current=True,
        )
        self.stream = Stream.objects.create(stream="Blue")
        self.primary_class = Class.objects.create(name="Primary 4", code="P4", section=self.primary_section)
        self.secondary_class = Class.objects.create(name="Senior 2", code="S2", section=self.secondary_section)
        AcademicClass.objects.create(
            section=self.primary_section,
            Class=self.primary_class,
            academic_year=self.year,
            term=self.term,
            fees_amount=100000,
        )
        AcademicClass.objects.create(
            section=self.secondary_section,
            Class=self.secondary_class,
            academic_year=self.year,
            term=self.term,
            fees_amount=120000,
        )
        self._create_student("Primary Student", self.primary_class)
        self._create_student("Secondary Student", self.secondary_class)

    def _create_student(self, name, class_obj):
        return Student.objects.create(
            reg_no="",
            student_name=name,
            gender="F",
            birthdate=date(2011, 1, 1),
            nationality="Ugandan",
            religion="Catholic",
            address="Kampala",
            guardian="Guardian",
            relationship="Parent",
            contact="0700000000",
            academic_year=self.year,
            current_class=class_obj,
            stream=self.stream,
            term=self.term,
            is_active=True,
        )

    def _request_for_level(self, level):
        request = self.factory.get("/")
        middleware = SessionMiddleware(lambda req: None)
        middleware.process_request(request)
        request.session["active_school_level"] = level
        request.session.save()
        return request

    def test_overview_context_scopes_to_secondary_mode(self):
        request = self._request_for_level(SchoolSetting.EducationLevel.SECONDARY_LOWER)
        scope = {"current_year": self.year, "current_term": self.term}

        context = get_overview_context(request, scope)

        self.assertEqual(context["total_students_active"], 1)
        self.assertEqual(context["total_classes"], 1)

    def test_overview_context_scopes_to_primary_mode(self):
        request = self._request_for_level(SchoolSetting.EducationLevel.PRIMARY)
        scope = {"current_year": self.year, "current_term": self.term}

        context = get_overview_context(request, scope)

        self.assertEqual(context["total_students_active"], 1)
        self.assertEqual(context["total_classes"], 1)


class SecondaryCBCComputationTests(TestCase):
    def setUp(self):
        self.section = Section.objects.create(section_name="Secondary")
        self.year = AcademicYear.objects.create(academic_year="2026", is_current=True)
        self.term = Term.objects.create(
            academic_year=self.year,
            term="2",
            start_date=date(2026, 5, 6),
            end_date=date(2026, 8, 2),
            is_current=True,
        )
        self.stream = Stream.objects.create(stream="Blue")
        self.class_obj = Class.objects.create(name="Senior 2", code="S2", section=self.section)
        Staff.objects.create(
            first_name="Grace",
            last_name="Teacher",
            birth_date=date(1990, 1, 1),
            gender="F",
            address="Kampala",
            marital_status="U",
            contacts="0700000000",
            email="grace.teacher@example.com",
            qualification="B.Ed",
            nin_no="CFX1234567890A",
            hire_date=date(2020, 1, 1),
            department="Academic",
            salary=Decimal("1200000.00"),
            is_academic_staff=True,
            is_administrator_staff=False,
            is_support_staff=False,
            staff_status="Active",
            staff_photo=SimpleUploadedFile("teacher.jpg", b"image-bytes", content_type="image/jpeg"),
        )
        self.academic_class = AcademicClass.objects.create(
            section=self.section,
            Class=self.class_obj,
            academic_year=self.year,
            term=self.term,
            fees_amount=100000,
        )
        self.student = Student.objects.create(
            reg_no="",
            student_name="Nakato Maria",
            gender="F",
            birthdate=date(2011, 3, 1),
            nationality="Ugandan",
            religion="Catholic",
            address="Kampala",
            guardian="Nakaayi Sarah",
            relationship="Mother",
            contact="0700000001",
            academic_year=self.year,
            current_class=self.class_obj,
            stream=self.stream,
            term=self.term,
        )
        self.subject = Subject.objects.create(
            code="MATH",
            name="Mathematics",
            description="Mathematics",
            credit_hours=4,
            section=self.section,
            type="Core",
        )
        self.policy = SecondaryComputationPolicy.objects.create(
            name="Uganda CBC",
            section=self.section,
            level=SecondaryComputationPolicy.Level.LOWER_SECONDARY,
            ca_weight=Decimal("20.00"),
            exam_weight=Decimal("80.00"),
            rounding_mode=SecondaryComputationPolicy.RoundingMode.ONE_DECIMAL,
            effective_from=date(2026, 1, 1),
            is_active=True,
        )
        SecondaryGradeBand.objects.bulk_create(
            [
                SecondaryGradeBand(
                    policy=self.policy,
                    grade="A",
                    descriptor="Exceptional",
                    min_score=Decimal("80.00"),
                    max_score=Decimal("100.00"),
                    display_order=1,
                ),
                SecondaryGradeBand(
                    policy=self.policy,
                    grade="B",
                    descriptor="Outstanding",
                    min_score=Decimal("70.00"),
                    max_score=Decimal("79.99"),
                    display_order=2,
                ),
                SecondaryGradeBand(
                    policy=self.policy,
                    grade="C",
                    descriptor="Satisfactory",
                    min_score=Decimal("60.00"),
                    max_score=Decimal("69.99"),
                    display_order=3,
                ),
                SecondaryGradeBand(
                    policy=self.policy,
                    grade="D",
                    descriptor="Basic",
                    min_score=Decimal("50.00"),
                    max_score=Decimal("59.99"),
                    display_order=4,
                ),
                SecondaryGradeBand(
                    policy=self.policy,
                    grade="E",
                    descriptor="Elementary",
                    min_score=Decimal("40.00"),
                    max_score=Decimal("49.99"),
                    display_order=5,
                ),
                SecondaryGradeBand(
                    policy=self.policy,
                    grade="F",
                    descriptor="Not Achieved",
                    min_score=Decimal("0.00"),
                    max_score=Decimal("39.99"),
                    display_order=6,
                ),
            ]
        )

    def _create_ca_record(self, title, weight, score, status=ContinuousAssessmentRecord.ModerationStatus.APPROVED):
        task = ContinuousAssessmentTask.objects.create(
            title=title,
            academic_class=self.academic_class,
            term=self.term,
            subject=self.subject,
            task_type=ContinuousAssessmentTask.TaskType.PROJECT,
            weight=Decimal(str(weight)),
            max_score=Decimal("100.00"),
            assigned_date=date(2026, 6, 1),
            uneb_eligible=True,
            status=ContinuousAssessmentTask.Status.APPROVED,
        )
        return ContinuousAssessmentRecord.objects.create(
            task=task,
            student=self.student,
            raw_score=Decimal(str(score)),
            moderation_status=status,
        )

    def test_secondary_weighted_result_matches_cbc_example(self):
        self._create_ca_record("Project", 30, 80)
        self._create_ca_record("Practical", 30, 70)
        self._create_ca_record("Coursework", 20, 75)
        self._create_ca_record("Portfolio", 20, 75)

        result = compute_subject_result(
            student=self.student,
            subject=self.subject,
            exam_score=Decimal("68.00"),
            policy=self.policy,
        )

        self.assertEqual(result["ca_score"], Decimal("75.00"))
        self.assertEqual(result["final_score"], Decimal("69.4"))
        self.assertEqual(result["grade"], "C")
        self.assertEqual(result["descriptor"], "Satisfactory")

    def test_uneb_submission_item_uses_uneb_eligible_records(self):
        self._create_ca_record("Eligible 1", 50, 80)
        self._create_ca_record("Eligible 2", 50, 70)
        non_uneb_task = ContinuousAssessmentTask.objects.create(
            title="Internal only",
            academic_class=self.academic_class,
            term=self.term,
            subject=self.subject,
            task_type=ContinuousAssessmentTask.TaskType.COURSEWORK,
            weight=Decimal("50.00"),
            max_score=Decimal("100.00"),
            assigned_date=date(2026, 6, 2),
            uneb_eligible=False,
            status=ContinuousAssessmentTask.Status.APPROVED,
        )
        ContinuousAssessmentRecord.objects.create(
            task=non_uneb_task,
            student=self.student,
            raw_score=Decimal("100.00"),
            moderation_status=ContinuousAssessmentRecord.ModerationStatus.APPROVED,
        )

        batch = UNEBSubmissionBatch.objects.create(
            title="S4 UNEB CA 2026",
            academic_year=self.year,
            section=self.section,
            status=UNEBSubmissionBatch.Status.DRAFT,
        )
        item = upsert_uneb_submission_item(
            batch=batch,
            student=self.student,
            subject=self.subject,
            policy=self.policy,
        )

        self.assertEqual(item.ca_mark, Decimal("75.00"))
        self.assertEqual(item.final_ca_mark, Decimal("75.0"))
        self.assertEqual(item.source_task_count, 2)

    def test_lock_batch_locks_submission_items_and_records(self):
        self._create_ca_record("Task 1", 100, 74)
        batch = UNEBSubmissionBatch.objects.create(
            title="S4 UNEB CA 2026",
            academic_year=self.year,
            section=self.section,
            status=UNEBSubmissionBatch.Status.SUBMITTED,
        )
        item = upsert_uneb_submission_item(
            batch=batch,
            student=self.student,
            subject=self.subject,
            policy=self.policy,
        )
        lock_uneb_batch(batch)
        item.refresh_from_db()
        batch.refresh_from_db()

        self.assertTrue(item.is_locked)
        self.assertEqual(batch.status, UNEBSubmissionBatch.Status.LOCKED)
        self.assertTrue(
            ContinuousAssessmentRecord.objects.filter(
                student=self.student,
                task__subject=self.subject,
                is_locked=True,
                moderation_status=ContinuousAssessmentRecord.ModerationStatus.LOCKED,
            ).exists()
        )

    def test_policy_requires_weights_to_total_100(self):
        bad_policy = SecondaryComputationPolicy(
            name="Bad policy",
            section=self.section,
            level=SecondaryComputationPolicy.Level.LOWER_SECONDARY,
            ca_weight=Decimal("35.00"),
            exam_weight=Decimal("80.00"),
            effective_from=date(2026, 1, 1),
        )
        with self.assertRaises(ValidationError):
            bad_policy.full_clean()


class SecondaryAccessModeTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(username="secondary-admin", password="testpass123")
        self.client.login(username="secondary-admin", password="testpass123")
        self.school_setting = SchoolSetting.objects.create(
            school_logo=SimpleUploadedFile("logo.jpg", b"logo-bytes", content_type="image/jpeg"),
            education_level=SchoolSetting.EducationLevel.PRIMARY,
        )

    def test_secondary_dashboard_requires_secondary_lower_mode(self):
        response = self.client.get(reverse("secondary:dashboard"))
        self.assertEqual(response.status_code, 302)
        self.assertIn(reverse("settings_page"), response.url)

    def test_secondary_dashboard_loads_when_secondary_lower_mode_is_enabled(self):
        self.school_setting.education_level = SchoolSetting.EducationLevel.SECONDARY_LOWER
        self.school_setting.save()

        response = self.client.get(reverse("secondary:dashboard"))
        self.assertEqual(response.status_code, 200)

    def test_primary_results_dashboard_is_blocked_in_secondary_mode(self):
        self.school_setting.education_level = SchoolSetting.EducationLevel.SECONDARY_LOWER
        self.school_setting.save()

        response = self.client.get(reverse("school_results_dashboard"))
        self.assertEqual(response.status_code, 302)
        self.assertIn(reverse("secondary:dashboard"), response.url)

    def test_session_level_switch_blocks_primary_routes_for_multi_level_school(self):
        self.school_setting.offers_primary = True
        self.school_setting.offers_secondary_lower = True
        self.school_setting.education_level = SchoolSetting.EducationLevel.PRIMARY
        self.school_setting.save()

        switch_response = self.client.post(
            reverse("switch_school_level"),
            {
                "active_school_level": SchoolSetting.EducationLevel.SECONDARY_LOWER,
                "next": reverse("index_page"),
            },
        )
        self.assertEqual(switch_response.status_code, 302)

        response = self.client.get(reverse("add_grading_system_page"))
        self.assertEqual(response.status_code, 302)
        self.assertIn(reverse("secondary:dashboard"), response.url)

    def test_alevel_pages_load_in_upper_mode(self):
        self.school_setting.education_level = SchoolSetting.EducationLevel.SECONDARY_UPPER
        self.school_setting.offers_primary = False
        self.school_setting.offers_secondary_upper = True
        self.school_setting.save()

        response = self.client.get(reverse("secondary:alevel_report_preview"))
        self.assertEqual(response.status_code, 200)

        lower_response = self.client.get(reverse("secondary:report_preview"))
        self.assertEqual(lower_response.status_code, 302)
        self.assertIn(reverse("secondary:alevel_report_preview"), lower_response.url)

    def test_lower_secondary_routes_are_blocked_in_upper_mode(self):
        self.school_setting.education_level = SchoolSetting.EducationLevel.SECONDARY_UPPER
        self.school_setting.offers_primary = False
        self.school_setting.offers_secondary_upper = True
        self.school_setting.save()

        response = self.client.get(reverse("secondary:ca_task_list"))
        self.assertEqual(response.status_code, 302)
        self.assertIn(reverse("secondary:dashboard"), response.url)


class ALevelComputationTests(TestCase):
    def setUp(self):
        self.section = Section.objects.create(section_name="Secondary")
        self.year = AcademicYear.objects.create(academic_year="2026", is_current=True)
        self.term = Term.objects.create(
            academic_year=self.year,
            term="2",
            start_date=date(2026, 5, 6),
            end_date=date(2026, 8, 2),
            is_current=True,
        )
        self.stream = Stream.objects.create(stream="A")
        self.class_obj = Class.objects.create(name="Senior 5", code="S5", section=self.section)
        Staff.objects.create(
            first_name="Isaac",
            last_name="Teacher",
            birth_date=date(1990, 1, 1),
            gender="M",
            address="Kampala",
            marital_status="U",
            contacts="0700000002",
            email="isaac.teacher@example.com",
            qualification="B.Ed",
            nin_no="CFX1234567891B",
            hire_date=date(2020, 1, 1),
            department="Academic",
            salary=Decimal("1200000.00"),
            is_academic_staff=True,
            is_administrator_staff=False,
            is_support_staff=False,
            staff_status="Active",
            staff_photo=SimpleUploadedFile("teacher2.jpg", b"image-bytes", content_type="image/jpeg"),
        )
        self.academic_class = AcademicClass.objects.create(
            section=self.section,
            Class=self.class_obj,
            academic_year=self.year,
            term=self.term,
            fees_amount=120000,
        )
        self.student = Student.objects.create(
            reg_no="",
            student_name="Nabirye Joan",
            gender="F",
            birthdate=date(2009, 4, 1),
            nationality="Ugandan",
            religion="Catholic",
            address="Kampala",
            guardian="Nabirye Sarah",
            relationship="Mother",
            contact="0700000003",
            academic_year=self.year,
            current_class=self.class_obj,
            stream=self.stream,
            term=self.term,
        )
        self.subject = Subject.objects.create(
            code="MATH-A",
            name="Mathematics",
            description="Advanced Mathematics",
            credit_hours=6,
            section=self.section,
            type="Core",
        )
        self.policy = SecondaryComputationPolicy.objects.create(
            name="Uganda A-Level CBC",
            section=self.section,
            level=SecondaryComputationPolicy.Level.UPPER_SECONDARY,
            ca_weight=Decimal("30.00"),
            exam_weight=Decimal("70.00"),
            rounding_mode=SecondaryComputationPolicy.RoundingMode.ONE_DECIMAL,
            effective_from=date(2026, 1, 1),
            is_active=True,
        )
        ALevelCompetencyScale.objects.bulk_create(
            [
                ALevelCompetencyScale(
                    policy=self.policy,
                    code=ALevelCompetencyScale.Code.EXCEEDS,
                    descriptor="Exceeds",
                    point_value=Decimal("4.00"),
                    min_weighted_point=Decimal("3.50"),
                    max_weighted_point=Decimal("4.00"),
                    display_order=1,
                ),
                ALevelCompetencyScale(
                    policy=self.policy,
                    code=ALevelCompetencyScale.Code.MEETS,
                    descriptor="Meets",
                    point_value=Decimal("3.00"),
                    min_weighted_point=Decimal("2.50"),
                    max_weighted_point=Decimal("3.49"),
                    display_order=2,
                ),
                ALevelCompetencyScale(
                    policy=self.policy,
                    code=ALevelCompetencyScale.Code.APPROACHING,
                    descriptor="Approaching",
                    point_value=Decimal("2.00"),
                    min_weighted_point=Decimal("1.50"),
                    max_weighted_point=Decimal("2.49"),
                    display_order=3,
                ),
                ALevelCompetencyScale(
                    policy=self.policy,
                    code=ALevelCompetencyScale.Code.BELOW,
                    descriptor="Below",
                    point_value=Decimal("1.00"),
                    min_weighted_point=Decimal("0.00"),
                    max_weighted_point=Decimal("1.49"),
                    display_order=4,
                ),
            ]
        )
        ALevelComponentWeight.objects.bulk_create(
            [
                ALevelComponentWeight(
                    policy=self.policy,
                    subject=self.subject,
                    component_type=ALevelComponentWeight.ComponentType.SCHOOL_BASED_ASSESSMENT,
                    weight=Decimal("30.00"),
                ),
                ALevelComponentWeight(
                    policy=self.policy,
                    subject=self.subject,
                    component_type=ALevelComponentWeight.ComponentType.MODULAR_EXAM,
                    weight=Decimal("50.00"),
                ),
                ALevelComponentWeight(
                    policy=self.policy,
                    subject=self.subject,
                    component_type=ALevelComponentWeight.ComponentType.PRACTICAL_PROJECT,
                    weight=Decimal("20.00"),
                ),
            ]
        )
        self.module_1 = ALevelSubjectModule.objects.create(
            subject=self.subject,
            code="M1",
            name="Calculus Module",
            module_order=1,
        )
        self.module_2 = ALevelSubjectModule.objects.create(
            subject=self.subject,
            code="M2",
            name="Statistics Module",
            module_order=2,
        )

    def test_a_level_computation_uses_latest_module_retake(self):
        ALevelModuleAssessment.objects.create(
            policy=self.policy,
            student=self.student,
            subject=self.subject,
            component_type=ALevelComponentWeight.ComponentType.SCHOOL_BASED_ASSESSMENT,
            competency_code=ALevelCompetencyScale.Code.MEETS,
        )
        ALevelModuleAssessment.objects.create(
            policy=self.policy,
            student=self.student,
            subject=self.subject,
            component_type=ALevelComponentWeight.ComponentType.PRACTICAL_PROJECT,
            competency_code=ALevelCompetencyScale.Code.MEETS,
        )
        ALevelModuleAssessment.objects.create(
            policy=self.policy,
            student=self.student,
            subject=self.subject,
            module=self.module_1,
            component_type=ALevelComponentWeight.ComponentType.MODULAR_EXAM,
            competency_code=ALevelCompetencyScale.Code.MEETS,
            attempt_no=1,
        )
        ALevelModuleAssessment.objects.create(
            policy=self.policy,
            student=self.student,
            subject=self.subject,
            module=self.module_1,
            component_type=ALevelComponentWeight.ComponentType.MODULAR_EXAM,
            competency_code=ALevelCompetencyScale.Code.EXCEEDS,
            attempt_no=2,
        )
        ALevelModuleAssessment.objects.create(
            policy=self.policy,
            student=self.student,
            subject=self.subject,
            module=self.module_2,
            component_type=ALevelComponentWeight.ComponentType.MODULAR_EXAM,
            competency_code=ALevelCompetencyScale.Code.EXCEEDS,
            attempt_no=1,
        )

        result = compute_a_level_subject_result(
            student=self.student,
            subject=self.subject,
            policy=self.policy,
        )

        self.assertEqual(result["weighted_point"], Decimal("3.50"))
        self.assertEqual(result["final_competency"], ALevelCompetencyScale.Code.EXCEEDS)
        self.assertEqual(
            result["component_points"][ALevelComponentWeight.ComponentType.MODULAR_EXAM],
            Decimal("4.00"),
        )
        self.assertEqual(len(result["modular_breakdown"]), 2)
