import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../data/models/exam_model.dart'; import '../../data/repositories/app_repositories.dart'; abstract class ExamState {} class ExamInitial extends ExamState {} class ExamLoading extends ExamState { final String message; ExamLoading({this.message = 'جاري تحضير الامتحان والأسئلة التكيفية...'}); } class ExamLoaded extends ExamState { final ExamModel exam; final int currentQuestionIndex; final Map selectedAnswers; // questionId -> optionId final int remainingSeconds; final bool isSubmitting; ExamLoaded({ required this.exam, this.currentQuestionIndex = 0, this.selectedAnswers = const {}, required this.remainingSeconds, this.isSubmitting = false, }); QuestionModel? get currentQuestion { if (exam.questions.isEmpty || currentQuestionIndex >= exam.questions.length) return null; return exam.questions[currentQuestionIndex]; } bool get isLastQuestion => currentQuestionIndex == exam.questions.length - 1; int get answeredCount => selectedAnswers.length; ExamLoaded copyWith({ ExamModel? exam, int? currentQuestionIndex, Map? selectedAnswers, int? remainingSeconds, bool? isSubmitting, }) { return ExamLoaded( exam: exam ?? this.exam, currentQuestionIndex: currentQuestionIndex ?? this.currentQuestionIndex, selectedAnswers: selectedAnswers ?? this.selectedAnswers, remainingSeconds: remainingSeconds ?? this.remainingSeconds, isSubmitting: isSubmitting ?? this.isSubmitting, ); } } class ExamCompleted extends ExamState { final ExamModel exam; final ExamSubmissionResultModel result; final int timeSpentSeconds; ExamCompleted({ required this.exam, required this.result, required this.timeSpentSeconds, }); } class ExamError extends ExamState { final String message; ExamError(this.message); } class ExamCubit extends Cubit { final ExamRepository _repository; Timer? _countdownTimer; int _initialSeconds = 0; ExamCubit({ExamRepository? repository}) : _repository = repository ?? ExamRepository(), super(ExamInitial()); @override Future close() { _countdownTimer?.cancel(); return super.close(); } Future loadExam({int examId = 1, ExamModel? initialExam}) async { emit(ExamLoading()); try { ExamModel loadedExam; if (initialExam != null && initialExam.questions.isNotEmpty) { loadedExam = initialExam; } else { try { loadedExam = await _repository.getExamDetails(examId); } catch (_) { // If server call fails or exam not in DB, use curriculum-grounded default questions loadedExam = initialExam ?? _createDefaultExam(examId); } } if (loadedExam.questions.isEmpty) { loadedExam = _createDefaultExam(examId, title: loadedExam.title); } _initialSeconds = loadedExam.durationMinutes * 60; emit(ExamLoaded( exam: loadedExam, remainingSeconds: _initialSeconds, )); _startTimer(); } catch (e) { emit(ExamError('حدث خطأ أثناء تحميل الامتحان: ${e.toString()}')); } } void _startTimer() { _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final s = state; if (s is ExamLoaded) { if (s.remainingSeconds <= 1) { timer.cancel(); submitCurrentExam(); } else { emit(s.copyWith(remainingSeconds: s.remainingSeconds - 1)); } } else { timer.cancel(); } }); } void selectOption(int questionId, int optionId) { final s = state; if (s is ExamLoaded) { final updatedAnswers = Map.from(s.selectedAnswers); updatedAnswers[questionId] = optionId; emit(s.copyWith(selectedAnswers: updatedAnswers)); } } void goToQuestion(int index) { final s = state; if (s is ExamLoaded) { if (index >= 0 && index < s.exam.questions.length) { emit(s.copyWith(currentQuestionIndex: index)); } } } void nextQuestion() { final s = state; if (s is ExamLoaded && !s.isLastQuestion) { emit(s.copyWith(currentQuestionIndex: s.currentQuestionIndex + 1)); } } void previousQuestion() { final s = state; if (s is ExamLoaded && s.currentQuestionIndex > 0) { emit(s.copyWith(currentQuestionIndex: s.currentQuestionIndex - 1)); } } Future submitCurrentExam() async { final s = state; if (s is! ExamLoaded) return; _countdownTimer?.cancel(); emit(s.copyWith(isSubmitting: true)); final timeSpent = _initialSeconds - s.remainingSeconds; final formattedAnswers = s.selectedAnswers.entries.map((e) { return { 'question_id': e.key, 'selected_option_id': e.value, }; }).toList(); try { ExamSubmissionResultModel result; try { result = await _repository.submitExam( s.exam.id, answers: formattedAnswers, timeSpentSeconds: timeSpent, ); } catch (_) { // Safe offline evaluation fallback if API token not present result = _evaluateLocally(s.exam, s.selectedAnswers); } emit(ExamCompleted( exam: s.exam, result: result, timeSpentSeconds: timeSpent, )); } catch (e) { emit(ExamError('فشل تقديم الامتحان: ${e.toString()}')); } } ExamSubmissionResultModel _evaluateLocally(ExamModel exam, Map selectedAnswers) { int earnedScore = 0; int totalScore = 0; List weakTopics = []; List detailedAnswers = []; for (var q in exam.questions) { totalScore += q.points; final selectedOptId = selectedAnswers[q.id]; QuestionOptionModel? correctOption; try { correctOption = q.options.firstWhere((opt) => opt.isCorrect); } catch (_) { correctOption = q.options.isNotEmpty ? q.options.first : null; } final isCorrect = selectedOptId != null && correctOption != null && selectedOptId == correctOption.id; final points = isCorrect ? q.points : 0; earnedScore += points; if (!isCorrect) { if (!weakTopics.contains(q.topicTag)) { weakTopics.add(q.topicTag); } } detailedAnswers.add(DetailedAnswerModel( questionId: q.id, selectedOptionId: selectedOptId ?? 0, isCorrect: isCorrect, pointsAwarded: points, explanation: q.explanationText ?? 'راجع نص القاعدة في كتاب الوزارة', aiHint: q.aiHint, )); } final pct = totalScore > 0 ? (earnedScore / totalScore) * 100 : 0.0; final passed = pct >= exam.passingPercentage; final report = passed ? 'أداء ممتاز! حققت نسبة إتقان ${pct.toStringAsFixed(1)}%. لديك استيعاب عميق للمفاهيم الأساسية.' : 'تم رصد تعثر في مفاهيم: ${weakTopics.join('، ')}. ننصح بمشاهدة مقاطع الشرح المركزة لمعالجة الثغرات.'; return ExamSubmissionResultModel( attemptId: DateTime.now().millisecondsSinceEpoch ~/ 1000, score: earnedScore, totalScore: totalScore, percentage: pct, passed: passed, rewindSeconds: passed ? 0 : 45, aiDiagnosticReport: report, weakTopics: weakTopics, tawjihiReadinessScore: (pct * 0.7) + (passed ? 25.0 : 10.0), detailedAnswers: detailedAnswers, ); } static ExamModel _createDefaultExam(int examId, {String? title}) { return ExamModel( id: examId, uuid: 'exam-default-$examId', courseId: 1, title: title ?? 'اختبار الفهم الشامل: الوحدة الأولى — أنظمة المعادلات', description: 'اختبار وزاري معياري يقيس الفهم العميق والقدرة على تطبيق القوانين لأنظمة المعادلات.', scope: 'unit_exam', passingPercentage: 70.0, durationMinutes: 45, questionsCount: 15, questions: const [ // 1. حل معادلات خاصة (إخراج العامل المشترك) QuestionModel( id: 1, questionText: 'ما هي مجموعة حل المعادلة الحقيقية: x³ - 4x = 0؟', questionType: 'multiple_choice', points: 10, topicTag: 'حل المعادلات بإخراج العامل المشترك الأكبر', explanationText: 'بإخراج x كعامل مشترك: x(x² - 4) = 0 ومنها x(x - 2)(x + 2) = 0، فيكون الحل x = 0, 2, -2.', options: [ QuestionOptionModel(id: 1, optionText: '{ -2, 0, 2 }', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 2, optionText: '{ 0, 4 }', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 3, optionText: '{ -2, 2 }', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 4, optionText: '{ 0, 2 }', isCorrect: false, sequenceOrder: 4), ], ), // 2. الصورة التربيعية QuestionModel( id: 2, questionText: 'حل المعادلة x⁴ - 5x² + 4 = 0 في مجموعة الأعداد الحقيقية هو:', questionType: 'multiple_choice', points: 10, topicTag: 'المعادلات في الصورة التربيعية', explanationText: 'بالتحليل كمعادلة تربيعية: (x² - 4)(x² - 1) = 0، ومنها x² = 4 أو x² = 1، فالجذور هي ±2 و ±1.', options: [ QuestionOptionModel(id: 5, optionText: '{ -2, -1, 1, 2 }', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 6, optionText: '{ 1, 4 }', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 7, optionText: '{ -1, 1 }', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 8, optionText: '{ -4, 4 }', isCorrect: false, sequenceOrder: 4), ], ), // 3. مجموع المكعبين والحلول الحقيقية QuestionModel( id: 3, questionText: 'كم حلاً حقيقياً يحقق المعادلة: x³ + 8 = 0؟', questionType: 'multiple_choice', points: 10, topicTag: 'تحليل مجموع المكعبين والحلول الحقيقية', explanationText: 'بتحليل مجموع مكعبين: (x + 2)(x² - 2x + 4) = 0. القوس التربيعي مميزه سالب (-12) فلا يعطي جذوراً حقيقية، والحل الحقيقي الوحيد هو x = -2.', options: [ QuestionOptionModel(id: 9, optionText: 'حل حقيقي واحد فقط وهو x = -2', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 10, optionText: 'ثلاثة حلول حقيقية', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 11, optionText: 'حلّان حقيقيان', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 12, optionText: 'لا يوجد أي حل حقيقي', isCorrect: false, sequenceOrder: 4), ], ), // 4. أقصى عدد حلول لنظام خطي وتربيعي QuestionModel( id: 4, questionText: 'ما هو أقصى عدد ممكن لنقاط التقاطع بين مستقيم وقطع مكافئ في المستوى الإحداثي؟', questionType: 'multiple_choice', points: 10, topicTag: 'التمثيل الهندسي لنظام خطي وتربيعي', explanationText: 'المستقيم يقطع القطع المكافئ في نقطتين كحد أقصى (حلان)، أو يمسه في نقطة (حل واحد)، أو لا يقطعه (لا يوجد حل حقيقي).', options: [ QuestionOptionModel(id: 13, optionText: 'نقطتان كحد أقصى (حلّان حقيقيان)', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 14, optionText: 'ثلاث نقاط تقاطع', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 15, optionText: 'أربع نقاط تقاطع', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 16, optionText: 'نقطة واحدة فقط دائماً', isCorrect: false, sequenceOrder: 4), ], ), // 5. حل نظام خطي وتربيعي بالتعويض QuestionModel( id: 5, questionText: 'إذا كان لدينا النظام: y = x + 1 و y = x² + 1، فما هي نقاط تقاطع المنحنيين؟', questionType: 'multiple_choice', points: 10, topicTag: 'حل نظام خطي وتربيعي بطريقة التعويض', explanationText: 'بالمساواة: x² + 1 = x + 1 ومنها x² - x = 0 أي x(x - 1) = 0، فيكون x = 0 أو x = 1. بالتعويض نجد y = 1 أو y = 2.', options: [ QuestionOptionModel(id: 17, optionText: '(0, 1) و (1, 2)', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 18, optionText: '(0, 0) و (1, 1)', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 19, optionText: '(1, 2) فقط', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 20, optionText: '(-1, 0) و (1, 2)', isCorrect: false, sequenceOrder: 4), ], ), // 6. المميز وسالبية الحلول QuestionModel( id: 6, questionText: 'متى لا يوجد أي حل حقيقي لنظام مكوّن من معادلة خطية وأخرى تربيعية؟', questionType: 'multiple_choice', points: 10, topicTag: 'استخدام المميز لتحديد عدد حلول النظام', explanationText: 'إذا كان مميز المعادلة التربيعية الناتجة عن التعويض سالباً (Δ = b² - 4ac < 0)، فإن المستقيم لا يتقاطع مع المنحنى.', options: [ QuestionOptionModel(id: 21, optionText: 'عندما يكون مميز المعادلة التربيعية الناتجة سالباً (Δ < 0)', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 22, optionText: 'عندما يكون المميز مساوياً لصفر', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 23, optionText: 'عندما يكون المميز موجباً تماماً', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 24, optionText: 'إذا كان ميل المستقيم يساوي صفراً', isCorrect: false, sequenceOrder: 4), ], ), // 7. حالة المماس QuestionModel( id: 7, questionText: 'إذا كان لنظام مكوّن من مستقيم وقطع مكافئ حل حقيقي وحيد فقط، فإن المستقيم يعتبر:', questionType: 'multiple_choice', points: 10, topicTag: 'المستقيم المماس لمنحنى تربيعي', explanationText: 'وجود حل حقيقي وحيد لنظام خطي-تربيعي يعني هندسياً أن المستقيم يمس المنحنى عند نقطة واحدة فقط.', options: [ QuestionOptionModel(id: 25, optionText: 'مماساً لمنحنى القطع المكافئ عند نقطة التماس', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 26, optionText: 'قاطعاً للمنحنى في نقطتين', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 27, optionText: 'خط تقارب رأسي للمنحنى', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 28, optionText: 'محور تماثل للقطع المكافئ', isCorrect: false, sequenceOrder: 4), ], ), // 8. حل نظام تربيعي بالحذف QuestionModel( id: 8, questionText: 'في النظام: x² + y² = 25 و x² - y² = 7، ما هي قيمة x²؟', questionType: 'multiple_choice', points: 10, topicTag: 'حل نظام تربيعي بطريقة الحذف', explanationText: 'بجمع المعادلتين طرفاً لطرف: 2x² = 32 ومنها x² = 16.', options: [ QuestionOptionModel(id: 29, optionText: 'x² = 16', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 30, optionText: 'x² = 9', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 31, optionText: 'x² = 32', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 32, optionText: 'x² = 18', isCorrect: false, sequenceOrder: 4), ], ), // 9. أقصى عدد حلول بين دائرة وقطع مكافئ QuestionModel( id: 9, questionText: 'ما هو أقصى عدد ممكن من نقاط التقاطع بين دائرة وقطع مكافئ في المستوى الإحداثي؟', questionType: 'multiple_choice', points: 10, topicTag: 'التقاطع الهندسي بين منحنيين تربيعيين', explanationText: 'يمكن لدائرة وقطع مكافئ أن يتقاطعا في 0، أو 1، أو 2، أو 3، أو 4 نقاط كحد أقصى.', options: [ QuestionOptionModel(id: 33, optionText: '4 نقاط تقاطع (أربعة حلول)', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 34, optionText: 'حلان فقط', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 35, optionText: '6 نقاط تقاطع', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 36, optionText: 'حل وحيد فقط دائماً', isCorrect: false, sequenceOrder: 4), ], ), // 10. التعويض بين معادلتين تربيعيتين QuestionModel( id: 10, questionText: 'النظام: x² + y² = 13 و y = x² + 1 يتقاطع في النقطتين:', questionType: 'multiple_choice', points: 10, topicTag: 'التعويض بين معادلتين تربيعيتين', explanationText: 'بتعويض x² = y - 1 في الأولى: y - 1 + y² = 13 أي y² + y - 14 = 0 أو نجد بالنظام y=3 و x=±2.', options: [ QuestionOptionModel(id: 37, optionText: '(-2, 3) و (2, 3)', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 38, optionText: '(3, -2) و (3, 2)', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 39, optionText: '(0, 1) فقط', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 40, optionText: '(2, 5) و (-2, 5)', isCorrect: false, sequenceOrder: 4), ], ), // 11. التناظر في أنظمة المعادلات QuestionModel( id: 11, questionText: 'إذا كانت النقطة (a, b) حلاً لنظام يتكون من دائرة مركزها نقطة الأصل ومعادلة متماثلة حول المحور الصادي، فإن:', questionType: 'multiple_choice', points: 10, topicTag: 'خصائص التناظر في أنظمة المعادلات', explanationText: 'التماثل حول محور الصادات يعني أن استبدال x بـ (-x) يعطي نفس النتيجة، فالنقطة (-a, b) تكون أيضاً حلاً للنظام.', options: [ QuestionOptionModel(id: 41, optionText: '(-a, b) تكون أيضاً حلاً للنظام', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 42, optionText: '(a, -b) هو الحل الوحيد دائماً', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 43, optionText: 'لا يوجد أي تناظر هندسي', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 44, optionText: 'الحل سالب دائماً', isCorrect: false, sequenceOrder: 4), ], ), // 12. برمجية جيوجبرا وأداة التقاطع QuestionModel( id: 12, questionText: 'في برمجية جيوجبرا (GeoGebra)، ما هي الأداة المخصصة لإيجاد حل نظام المعادلات بيانياً؟', questionType: 'multiple_choice', points: 10, topicTag: 'برمجية جيوجبرا وحل الأنظمة بيانياً', explanationText: 'أداة التقاطع (Intersect Tool) في جيوجبرا تُحدد إحداثيات نقاط تقاطع المنحنيات التي تمثل حلول النظام مباشرة.', options: [ QuestionOptionModel(id: 45, optionText: 'أداة التقاطع (Intersect Tool)', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 46, optionText: 'أداة القياس (Measure)', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 47, optionText: 'أداة الانعكاس (Reflect)', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 48, optionText: 'أداة المماس (Tangent)', isCorrect: false, sequenceOrder: 4), ], ), // 13. قراءة الإحداثيات في جيوجبرا QuestionModel( id: 13, questionText: 'عند تمثيل معادلتين في جيوجبرا وظهور نقطة التقاطع A = (3, -2)، فهذا يعني هندسياً وجبرياً أن:', questionType: 'multiple_choice', points: 10, topicTag: 'تفسير مخرجات برمجية جيوجبرا', explanationText: 'إحداثيات نقطة التقاطع (x, y) تعني أن الزوج المرتب يحقق كلتا المعادلتين معاً في آن واحد، وهو حل النظام.', options: [ QuestionOptionModel(id: 49, optionText: 'x = 3 و y = -2 هو حل يحقق كلتا المعادلتين معاً', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 50, optionText: 'x = -2 و y = 3 هو الحل', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 51, optionText: 'المنحنيان متباعدان ولا حل لهما', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 52, optionText: 'النظام له حلول غير منتهية', isCorrect: false, sequenceOrder: 4), ], ), // 14. النماذج الحياتية والأرصاد QuestionModel( id: 14, questionText: 'لماذا يستخدم خبراء الأرصاد الجوية أنظمة معادلات غير خطية في التنبؤ بالطقس كما ورد في كتاب الطالب؟', questionType: 'multiple_choice', points: 10, topicTag: 'التطبيقات الحياتية لأنظمة المعادلات', explanationText: 'لأن أي تغير في أحد العوامل (كالضغط ودرجة الحرارة وسرعة الرياح) يؤدي إلى تغير غير خطي في العوامل الأخرى.', options: [ QuestionOptionModel(id: 53, optionText: 'لأن أي تغير في أحد العوامل يؤدي إلى تغير غير خطي في العوامل الأخرى', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 54, optionText: 'لأن درجة الحرارة ثابتة دائماً على مدار السنة', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 55, optionText: 'للتخلص من قياس الضغط الجوي', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 56, optionText: 'لأن سرعة الرياح لا ترتبط بحركة الغلاف الجوي', isCorrect: false, sequenceOrder: 4), ], ), // 15. مسألة هندسية وتطبيقية QuestionModel( id: 15, questionText: 'سياج مستطيل الشكل محيطه 20 متراً ومساحته 24 متراً مربعاً. ما هما بعدا المستطيل؟', questionType: 'multiple_choice', points: 10, topicTag: 'حل المسائل الهندسية الحياتية باستخدام الأنظمة', explanationText: 'النظام: 2(x + y) = 20 ومنها x + y = 10، والمساحة x * y = 24. العددان اللذان مجموعهما 10 وحاصل ضربهما 24 هما 6 و 4.', options: [ QuestionOptionModel(id: 57, optionText: 'الطول 6 m والعرض 4 m', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 58, optionText: 'الطول 8 m والعرض 2 m', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 59, optionText: 'الطول 10 m والعرض 2.4 m', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 60, optionText: 'الطول 5 m والعرض 5 m', isCorrect: false, sequenceOrder: 4), ], ), ], ); } }