import 'dart:convert'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_backend/tests/generators/models/creation_test_data.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:drift/drift.dart' as drift; import 'generators/pack_test_generator.dart'; @lazySingleton class TestManager { final AppDatabase _db; TestManager(this._db); Future _testStatisticsDto( String userId, String testId) async { final statistics = await _db.testDao.getTestStatistics(userId, testId); if (statistics == null) return null; // Convert TestStatistic to TestStatisticsDto // metadata is stored as JSON string with 'attempts' key final metadataMap = statistics.metadata.isNotEmpty ? (json.decode(statistics.metadata) as Map? ?? {}) : {}; final attempts = (metadataMap['attempts'] as List?) ?? []; // Get words from the last attempt, or empty list if no attempts final lastAttempt = attempts.isNotEmpty ? attempts.last as Map? : null; final wordsJson = (lastAttempt?['words'] as List?) ?? []; // Convert words JSON to WordStatisticsDto final words = wordsJson .map((w) => WordStatisticsDto.fromJson(w as Map)) .toList(); return TestStatisticsDto( testId: statistics.testId, words: AllWordsStatisticsDto(words: words), attempts: attempts.length, sessionToken: lastAttempt?['sessionToken'] as String?, ); } Future fetchTest(String id, UserModel user) async { final testId = id; final test = await _db.testDao.getTestById(testId); if (test == null) return null; // Get packId for the test to convert image IDs to URLs final packId = await _db.testDao.getPackIdForTest(testId); // Helper function to convert image ID to URL String? _convertImageToUrl(String? imageValue, String? packId) { if (imageValue == null || packId == null) return imageValue; // If it's already a proper URL, return as is if (imageValue.startsWith('http://') || imageValue.startsWith('https://') || (imageValue.startsWith('/api/') && imageValue.contains('/cards/') && imageValue.endsWith('/image'))) { return imageValue; } // If it looks like a UUID (card ID), convert to URL if (RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false).hasMatch(imageValue)) { return '/api/v2/packs/$packId/cards/$imageValue/image'; } // Otherwise, assume it's already a card ID and convert return '/api/v2/packs/$packId/cards/$imageValue/image'; } final questions = await _db.testDao.getTestQuestions(testId); final statistics = await _testStatisticsDto(user.id!, testId); // Convert Test to TestDto final questionsList = questions.map((q) { // Build question JSON from separate fields final questionJson = { 'questionType': q.questionType, 'id': q.id, 'word': q.word, }; // Parse options (JSON array of buttons) List buttons = []; try { buttons = json.decode(q.options) as List; } catch (e) { buttons = []; } // Convert button images to URLs final buttonsWithUrls = buttons.map((button) { if (button is Map && button['image'] != null) { final buttonMap = Map.from(button); buttonMap['image'] = _convertImageToUrl( buttonMap['image'] as String?, packId, ); return buttonMap; } return button; }).toList(); questionJson['buttons'] = buttonsWithUrls; // Add answer questionJson['answer'] = q.answer; // Parse uiData (image, text, audio, template) try { final uiData = json.decode(q.uiData) as Map; // Convert question image to URL if (uiData['image'] != null) { uiData['image'] = _convertImageToUrl( uiData['image'] as String?, packId, ); } questionJson.addAll(uiData); } catch (e) { // If uiData is empty or invalid, ignore } return AbstractTestQuestion.fromJson(questionJson); }).toList(); return TestDto( id: testId.toString(), name: test.name, color: test.color, cover: test.cover, version: test.version ?? '1.0', time: test.time, timeSubtitle: test.timeSubtitle, questions: questionsList, statistics: statistics, ); } Future> availableTests(UserModel userModel) async { final tests = await _db.testDao.getAllTests(); final testDtos = []; for (final test in tests) { final questions = await _db.testDao.getTestQuestions(test.id); final statistics = await _testStatisticsDto(userModel.id!, test.id); final questionsList = questions.map((q) { // Build question JSON from separate fields final questionJson = { 'questionType': q.questionType, 'id': q.id, 'word': q.word, }; // Parse options (JSON array of buttons) try { final options = json.decode(q.options) as List; questionJson['buttons'] = options; } catch (e) { questionJson['buttons'] = []; } // Add answer questionJson['answer'] = q.answer; // Parse uiData (image, text, audio, template) try { final uiData = json.decode(q.uiData) as Map; questionJson.addAll(uiData); } catch (e) { // If uiData is empty or invalid, ignore } return AbstractTestQuestion.fromJson(questionJson); }).toList(); testDtos.add(TestDto( id: test.id.toString(), name: test.name, color: test.color, cover: test.cover, version: test.version ?? '1.0', time: test.time, timeSubtitle: test.timeSubtitle, questions: questionsList, statistics: statistics, )); } return testDtos; } Future> fetchPackTests( UserModel user, CardPackModel model, ) async { final packId = model.id; if (packId == null) return []; final tests = await _db.testDao.getTestsByPackId(packId); // Helper function to convert image ID to URL String? _convertImageToUrl(String? imageValue, String? packId) { if (imageValue == null || packId == null) return imageValue; // If it's already a proper URL, return as is if (imageValue.startsWith('http://') || imageValue.startsWith('https://') || (imageValue.startsWith('/api/') && imageValue.contains('/cards/') && imageValue.endsWith('/image'))) { return imageValue; } // If it looks like a UUID (card ID), convert to URL if (RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false).hasMatch(imageValue)) { return '/api/v2/packs/$packId/cards/$imageValue/image'; } // Otherwise, assume it's already a card ID and convert return '/api/v2/packs/$packId/cards/$imageValue/image'; } final testDtos = []; for (final test in tests) { final questions = await _db.testDao.getTestQuestions(test.id); final statistics = await _testStatisticsDto(user.id!, test.id); final questionsList = questions.map((q) { // Build question JSON from separate fields final questionJson = { 'questionType': q.questionType, 'id': q.id, 'word': q.word, }; // Parse options (JSON array of buttons) List buttons = []; try { buttons = json.decode(q.options) as List; } catch (e) { buttons = []; } // Convert button images to URLs final buttonsWithUrls = buttons.map((button) { if (button is Map && button['image'] != null) { final buttonMap = Map.from(button); buttonMap['image'] = _convertImageToUrl( buttonMap['image'] as String?, packId, ); return buttonMap; } return button; }).toList(); questionJson['buttons'] = buttonsWithUrls; // Add answer questionJson['answer'] = q.answer; // Parse uiData (image, text, audio, template) try { final uiData = json.decode(q.uiData) as Map; // Convert question image to URL if (uiData['image'] != null) { uiData['image'] = _convertImageToUrl( uiData['image'] as String?, packId, ); } questionJson.addAll(uiData); } catch (e) { // If uiData is empty or invalid, ignore } return AbstractTestQuestion.fromJson(questionJson); }).toList(); testDtos.add(TestDto( id: test.id.toString(), name: test.name, color: test.color, cover: test.cover, version: test.version ?? '1.0', time: test.time, timeSubtitle: test.timeSubtitle, questions: questionsList, statistics: statistics, )); } return testDtos; } Future refreshCustomCreationTestsData() async { // Load custom creation test data from database // For now, keep it simple } Future updateGeneratedTests(CardPackModel model) async { final packId = model.id; if (packId == null) return; // Get pack data from database final pack = await _db.packDao.getPackById(packId); if (pack == null) return; // Get all cards for the pack final cards = await _db.packDao.getPackCards(packId); if (cards.isEmpty) return; // Delete old generated tests for this pack final existingTests = await _db.testDao.getTestsByPackId(packId); final oldGeneratedTests = existingTests .where((test) => test.version == 'generated') .toList(); for (final oldTest in oldGeneratedTests) { await _db.testDao.softDeleteTest(oldTest.id); } // Create creation test data final testDataItems = cards.map((card) { return TestDataItem( id: card.id.toString(), original: card.original, translation: card.translation, image: card.image, audio: card.original, // Use original as audio ); }).toList(); final creationTestData = CreationTestData( items: testDataItems, title: pack.title, color: pack.color, packId: packId.toString(), ); // Generate test using PackTestGenerator final generator = PackTestGenerator(creationTestData); final testDto = await generator.generate( name: '${pack.title} Test', ratios: { TestQuestionType.simple: 0.7, TestQuestionType.input_buttons: 0.3, }, multiply: 1.0, ); // Save test to database and link it to the pack in one transaction. await addTest( testDto, packId: packId, ); } Future addTest( TestDto testDto, { String? packId, }) async { String? createdTestId; await _db.transaction(() async { // Create test final testCompanion = TestsCompanion.insert( name: testDto.name, color: drift.Value(testDto.color), cover: drift.Value(testDto.cover), version: drift.Value(testDto.version), time: drift.Value(testDto.time), timeSubtitle: drift.Value(testDto.timeSubtitle), ); createdTestId = await _db.testDao.createTest(testCompanion); final testId = createdTestId!; if (packId != null) { await _db.testDao.linkTestToPack(testId, packId); } // Create questions int orderIndex = 0; for (final question in testDto.questions) { final questionJson = question.toJson(); // Extract key fields final word = questionJson['word'] as String? ?? ''; final answer = questionJson['answer'] as String? ?? ''; final buttons = questionJson['buttons'] as List? ?? []; // UI data (image, text, audio, template) final uiData = {}; if (questionJson['image'] != null) uiData['image'] = questionJson['image']; if (questionJson['text'] != null) uiData['text'] = questionJson['text']; if (questionJson['audio'] != null) uiData['audio'] = questionJson['audio']; if (questionJson['template'] != null) uiData['template'] = questionJson['template']; final questionCompanion = TestQuestionsCompanion.insert( testId: testId, orderIndex: drift.Value(orderIndex++), questionType: question.questionType.name, word: word, answer: answer, options: drift.Value(json.encode(buttons)), uiData: drift.Value(json.encode(uiData)), ); await _db.testDao.createTestQuestion(questionCompanion); } }); return createdTestId!; } }