import 'package:flutter_test/flutter_test.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart'; void main() { test('maps localized rocket rule html from go rule response', () { final previousLang = SCGlobalConfig.lang; SCGlobalConfig.lang = 'zh'; try { final sections = RoomRocketApiMapper.noteSectionsFromRule({ 'zh-CN': '

火箭规则说明

', 'en': '

Rocket rules

', }); expect(sections, isNotNull); expect(sections?.single.body, '

火箭规则说明

'); expect(sections?.single.isHtml, isTrue); } finally { SCGlobalConfig.lang = previousLang; } }); test('maps rocket rule html from config ruleText fallback', () { final previousLang = SCGlobalConfig.lang; SCGlobalConfig.lang = 'ar'; try { final sections = RoomRocketApiMapper.noteSectionsFromRule({ 'ruleText': { 'zh-CN': '

火箭规则说明

', 'en': '

Rocket rules

', }, }); expect(sections?.single.body, '

Rocket rules

'); expect(sections?.single.isHtml, isTrue); } finally { SCGlobalConfig.lang = previousLang; } }); test('normalizes escaped newline sequences from rocket rule response', () { final previousLang = SCGlobalConfig.lang; SCGlobalConfig.lang = 'en'; try { final sections = RoomRocketApiMapper.noteSectionsFromRule({ 'en': '

Line 1\\nLine 2

', }); expect(sections?.single.body, '

Line 1\nLine 2

'); expect(sections?.single.isHtml, isTrue); } finally { SCGlobalConfig.lang = previousLang; } }); test('formats rocket rule html lists without detached markers', () { final text = debugRoomRocketRuleHtmlPlainText( '

How to Launch a Rocket

' '
    ' '
  1. Sending gifts in the room will add fuel.

  2. ' '
  3. Once the rocket fuel is full, it will launch.

  4. ' '
', ); expect(text, contains('How to Launch a Rocket\n1. Sending gifts')); expect(text, contains('\n2. Once the rocket fuel is full')); expect(text, isNot(contains('1. \n'))); expect(text, isNot(contains('2. \n'))); }); test('preserves newline characters inside rocket rule html text', () { final text = debugRoomRocketRuleHtmlPlainText( '

1. Sending gifts in the room will add fuel.\n' '2. Once the rocket fuel is full, it will launch.

', ); expect( text, '1. Sending gifts in the room will add fuel.\n' '2. Once the rocket fuel is full, it will launch.', ); }); test('keeps manually numbered rocket rule html from duplicating markers', () { final text = debugRoomRocketRuleHtmlPlainText( '

How to Launch a Rocket

' '
    ' '
  1. 1. Sending gifts in the room will add fuel.

  2. ' '
  3. 2. Once the rocket fuel is full, it will launch.

  4. ' '
  5. 3. The higher the rocket level, the more rewards.

  6. ' '
  7. Leaderboard and Rewards

  8. ' '
' '
    ' '
  1. 1. The Top 1 user will receive an exclusive reward.

  2. ' '
', ); expect(text, contains('How to Launch a Rocket\n1. Sending gifts')); expect(text, contains('\nLeaderboard and Rewards\n1. The Top 1 user')); expect(text, isNot(contains('1. \n1.'))); expect(text, isNot(contains('4. \nLeaderboard'))); expect(text, isNot(contains('4. Leaderboard'))); }); test('groups unread reward popups by launch number', () { final groups = RoomRocketApiMapper.rewardPopupRecordGroups([ _rewardRecord(id: 'r1', launchNo: 'launch-a'), _rewardRecord(id: 'r2', launchNo: 'launch-a'), _rewardRecord(id: 'r3', launchNo: 'launch-b'), _rewardRecord(id: 'r4', launchNo: ''), _rewardRecord(id: '', launchNo: ''), ]); expect(groups.map((group) => group.length), [2, 1, 1, 1]); expect(groups.first.map((record) => record.id), ['r1', 'r2']); expect(groups[1].single.launchNo, 'launch-b'); }); test('matches room winner records to the current popup batch', () { final roomRecords = [ _rewardRecord(id: 'room-a', launchNo: 'launch-a'), _rewardRecord(id: 'room-b', launchNo: 'launch-b'), ]; final matched = RoomRocketApiMapper.rewardRoomRecordsForPopupGroup( roomRecords, [_rewardRecord(id: 'popup-b', launchNo: 'launch-b')], ); expect(matched, hasLength(1)); expect(matched.single.id, 'room-b'); }); test( 'parses level-specific reward previews and rocket kings from levels', () { final status = SCRoomRocketStatusRes.fromJson({ 'currentLevel': 1, 'levels': [ { 'level': 1, 'rewardPreview': { 'top1': [ { 'rewardScene': 'TOP1', 'rewardType': 'GOLD', 'rewardAmount': 100, }, ], }, 'rocketKings': [ {'rank': 1, 'userId': 'u1', 'userAvatar': 'avatar-1'}, ], }, { 'level': 2, 'rewardPreview': { 'ignite': [ { 'rewardScene': 'IGNITE', 'rewardType': 'GOLD', 'rewardAmount': 200, }, ], }, 'rocketKings': [ {'rank': 1, 'userId': 'u2', 'userAvatar': 'avatar-2'}, {'rank': 2, 'userId': 'u3', 'userAvatar': 'avatar-3'}, {'rank': 3, 'userId': 'u4', 'userAvatar': 'avatar-4'}, ], }, ], }); expect(status.rewardPreviewForLevel(1)?.top1.single.rewardAmount, 100); expect(status.rewardPreviewForLevel(2)?.ignite.single.rewardAmount, 200); expect(status.rocketKingsForLevel(1).single.userAvatar, 'avatar-1'); expect(status.rocketKingsForLevel(2), hasLength(3)); }, ); test('parses level-specific reward previews and rocket kings from maps', () { final status = SCRoomRocketStatusRes.fromJson({ 'currentLevel': 1, 'rewardPreview': { '1': { 'top1': [ {'rewardScene': 'TOP1', 'rewardType': 'GOLD', 'rewardAmount': 100}, ], }, '2': { 'inRoom': [ { 'rewardScene': 'IN_ROOM', 'rewardType': 'GOLD', 'rewardAmount': 300, }, ], }, }, 'rocketKings': { '1': [ {'rank': 1, 'userId': 'u1', 'userAvatar': 'avatar-1'}, ], '2': [ {'rank': 1, 'userId': 'u2', 'userAvatar': 'avatar-2'}, {'rank': 2, 'userId': 'u3', 'userAvatar': 'avatar-3'}, {'rank': 3, 'userId': 'u4', 'userAvatar': 'avatar-4'}, ], }, }); expect(status.rewardPreviewForLevel(1)?.top1.single.rewardAmount, 100); expect(status.rewardPreviewForLevel(2)?.inRoom.single.rewardAmount, 300); expect(status.rocketKingsForLevel(1).single.userAvatar, 'avatar-1'); expect(status.rocketKingsForLevel(2), hasLength(3)); }); test('parses refreshed rocket status from snake case payloads', () { final status = SCRoomRocketStatusRes.fromJson({ 'room_id': 'room-1', 'current_level': 2, 'display_percent': 0, 'current_energy': 0, 'need_energy': 200, 'round_no': 4, 'levels': [ { 'level': 1, 'rocket_icon_url': 'icon-1', 'progress_bar_url': 'progress-1', }, { 'level': 2, 'rocket_icon_url': 'icon-2', 'progress_bar_url': 'progress-2', }, ], }); final levels = RoomRocketApiMapper.levelsFromStatus(status); expect(status.roomId, 'room-1'); expect(status.currentLevel, 2); expect(status.displayPercent, 0); expect(status.needEnergy, 200); expect(status.roundNo, 4); expect(levels?.last.imageUrl, 'icon-2'); expect(levels?.last.progressImageUrl, 'progress-2'); }); test('parses realtime level advancement payloads', () { final status = SCRoomRocketStatusRes.fromJson({ 'room_id': 'room-1', 'from_level': 1, 'to_level': 2, 'display_percent': 0, }); expect(status.currentLevel, 2); expect(status.level, 2); expect(status.displayPercent, 0); }); test('parses realtime current rocket level aliases', () { final status = SCRoomRocketStatusRes.fromJson({ 'room_id': 'room-1', 'current_rocket_level': 2, 'display_percent': 0, }); expect(status.currentLevel, 2); expect(status.level, 2); }); test('merges realtime rocket status without dropping level assets', () { final current = SCRoomRocketStatusRes.fromJson({ 'room_id': 'room-1', 'current_level': 1, 'display_percent': 98, 'levels': [ { 'level': 1, 'rocket_icon_url': 'icon-1', 'progress_bar_url': 'progress-1', }, { 'level': 2, 'rocket_icon_url': 'icon-2', 'progress_bar_url': 'progress-2', }, ], }); final realtimeUpdate = SCRoomRocketStatusRes.fromJson({ 'room_id': 'room-1', 'to_level': 2, 'display_percent': 0, }); final merged = current.mergeRealtimeUpdate(realtimeUpdate); final levels = RoomRocketApiMapper.levelsFromStatus(merged); expect(merged.currentLevel, 2); expect(merged.displayPercent, 0); expect(levels?.last.imageUrl, 'icon-2'); expect(levels?.last.progressImageUrl, 'progress-2'); }); test('parses disabled rocket status from go api', () { final status = SCRoomRocketStatusRes.fromJson({ 'configured': true, 'enabled': false, 'status': 'DISABLED', 'roomId': 'room-1', 'levels': [], }); expect(status.configured, isTrue); expect(status.enabled, isFalse); expect(status.isEnabled, isFalse); expect(status.status, 'DISABLED'); expect(RoomRocketApiMapper.levelsFromStatus(status), isNull); }); test('treats unconfigured or disabled-status rocket as unavailable', () { final unconfigured = SCRoomRocketStatusRes.fromJson({ 'configured': false, 'enabled': true, 'status': 'ENABLED', }); final disabledStatus = SCRoomRocketStatusRes.fromJson({ 'configured': true, 'status': 'DISABLED', }); expect(unconfigured.isEnabled, isFalse); expect(disabledStatus.isEnabled, isFalse); }); test( 'uses top-level reward preview for the response level instead of current level', () { final status = SCRoomRocketStatusRes.fromJson({ 'currentLevel': 1, 'level': 2, 'rewardPreview': { 'top1': [ {'rewardScene': 'TOP1', 'rewardType': 'GOLD', 'rewardAmount': 200}, ], }, }); expect(status.rewardPreviewForLevel(1), isNull); expect(status.rewardPreviewForLevel(2)?.top1.single.rewardAmount, 200); }, ); test('maps enabled rocket configs into level-specific reward groups', () { final groups = RoomRocketApiMapper.rewardGroupsByLevelFromConfigs([ SCRoomRocketConfigRes.fromJson({ 'level': 1, 'rewardConfig': [ {'type': 'GOLD', 'detailType': 'GOLD', 'amount': 100, 'sort': 1}, ], }), SCRoomRocketConfigRes.fromJson({ 'level': 2, 'rewardConfig': [ {'type': 'GOLD', 'detailType': 'GOLD', 'amount': 200, 'sort': 1}, ], 'normalRewardConfig': [ { 'type': 'PROPS', 'detailType': 'AVATAR_FRAME', 'cover': 'cover-2', 'quantity': 7, 'sort': 2, }, ], }), ]); expect(groups[1]?.top1.single.amount, '100'); expect(groups[2]?.top1.single.amount, '200'); expect(groups[2]?.inRoom.single.imageUrl, 'cover-2'); expect(groups[2]?.inRoom.single.expireDays, 7); }); test('normalizes non-gold rocket reward days display text', () { final rewards = [ SCRoomRocketRewardRes.fromJson({ 'rewardType': 'PROPS', 'rewardAmount': '7d', }), SCRoomRocketRewardRes.fromJson({ 'rewardType': 'PROPS', 'rewardAmount': 1, }), SCRoomRocketRewardRes.fromJson({ 'rewardType': 'PROPS', 'rewardAmount': 1, 'expireDays': '30D', }), SCRoomRocketRewardRes.fromJson({ 'rewardType': 'GOLD', 'rewardAmount': 100, }), ].map(RoomRocketApiMapper.rewardItemFromPreview).toList(growable: false); expect(rewards[0].displayText, '7d'); expect(rewards[1].displayText, '1d'); expect(rewards[2].displayText, '30d'); expect(rewards[3].displayText, '100'); final recordRewards = RoomRocketApiMapper.rewardItemsFromRecords([ SCRoomRocketRewardRecordRes.fromJson({ 'rewardType': 'PROPS', 'rewardAmount': '7d', }), ]); expect(recordRewards.single.displayText, '7d'); }); test('parses rocket reward winner avatars from nested resource payloads', () { final winners = RoomRocketApiMapper.winnersFromRecords([ SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r1', 'userId': 'u1', 'userInfo': { 'nickname': 'winner', 'avatar': {'sourceUrl': 'https://example.com/avatar.png'}, }, 'rewardType': 'GOLD', 'rewardAmount': 100, }), ]); expect(winners.single.nickname, 'winner'); expect(winners.single.avatarUrl, 'https://example.com/avatar.png'); }); test('parses rocket reward winner profile from receiver aliases', () { final record = SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r1', 'acceptUserId': 'u1', 'acceptNickname': 'winner', 'acceptUserAvatar': {'cdn_url': 'https://example.com/avatar.png'}, 'rewardType': 'GOLD', 'rewardAmount': 100, }); final winners = RoomRocketApiMapper.winnersFromRecords([record]); expect(record.userId, 'u1'); expect(record.displayName, 'winner'); expect(winners.single.nickname, 'winner'); expect(winners.single.avatarUrl, 'https://example.com/avatar.png'); }); test('hydrates rocket reward winners from room user profiles', () { final records = [ SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r1', 'userId': '2042274349', 'rewardType': 'GOLD', 'rewardAmount': 100, }), ]; final winners = RoomRocketApiMapper.winnersFromRecords( records, userProfileResolver: (_) => const RoomRocketRewardUserProfile( userId: 'u1', account: '2042274349', nickname: 'winner', avatarUrl: 'https://example.com/avatar.png', ), ); expect(records.single.displayName, 'User'); expect(winners.single.nickname, 'winner'); expect(winners.single.avatarUrl, 'https://example.com/avatar.png'); }); test('marks rocket reward winners by reward scene', () { final winners = RoomRocketApiMapper.winnersFromRecords([ SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r1', 'userId': 'top-user', 'userNickname': 'top winner', 'rewardScene': 'TOP1', 'rewardType': 'GOLD', 'rewardAmount': 100, }), SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r2', 'userId': 'ignite-user', 'userNickname': 'igniter', 'rewardScene': 'SET_OFF', 'rewardType': 'PROPS', 'rewardAmount': 1, }), ]); expect(winners.first.isTop1, isTrue); expect(winners.first.isIgniter, isFalse); expect(winners.last.isTop1, isFalse); expect(winners.last.isIgniter, isTrue); }); test('keeps nested rocket reward users separated by profile id', () { final winners = RoomRocketApiMapper.winnersFromRecords([ SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r1', 'launchNo': 'launch-1', 'userInfo': {'id': 'u1', 'nickname': 'winner 1'}, 'rewardType': 'GOLD', 'rewardAmount': 100, }), SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r2', 'launchNo': 'launch-1', 'userInfo': {'id': 'u2', 'nickname': 'winner 2'}, 'rewardType': 'GOLD', 'rewardAmount': 100, }), ]); expect(winners, hasLength(2)); expect(winners.map((item) => item.nickname), ['winner 1', 'winner 2']); }); test('parses room reward records from alternate response keys', () { final records = SCRoomRocketRewardRecordsRes.fromJson({ 'room_reward_records': [ { 'id': 'r1', 'launch_no': 'launch-1', 'room_id': 'room-1', 'user_info': {'id': 'u1', 'user_nickname': 'winner 1'}, 'reward_type': 'GOLD', 'reward_amount': 100, }, { 'id': 'r2', 'launch_no': 'launch-1', 'room_id': 'room-1', 'user_info': {'id': 'u2', 'user_nickname': 'winner 2'}, 'reward_type': 'GOLD', 'reward_amount': 200, }, ], }).records; final winners = RoomRocketApiMapper.winnersFromRecords(records); expect(records.map((item) => item.userId), ['u1', 'u2']); expect(winners, hasLength(2)); expect(winners.last.rewards.single.amount, '200'); }); test('parses nested rocket reward record resource payloads', () { final record = SCRoomRocketRewardRecordRes.fromJson({ 'id': 'r1', 'launchNo': 'launch-1', 'roomId': 'room-1', 'userInfo': {'id': 'u1', 'nickname': 'winner'}, 'reward': { 'detailType': 'AVATAR_FRAME', 'amount': '1969014438069481474', 'quantity': 7, 'name': 'yumi-KingTOP1 frame', 'cover': {'sourceUrl': 'https://example.com/frame.png'}, }, }); final reward = RoomRocketApiMapper.rewardItemsFromRecords([record]).single; expect(record.rewardType, 'AVATAR_FRAME'); expect(record.rewardItemId, isNotEmpty); expect(record.rewardName, 'yumi-KingTOP1 frame'); expect(record.rewardCover, 'https://example.com/frame.png'); expect(record.expireDays, 7); expect(reward.displayText, '7d'); }); test('parses rocket level contents response from go api', () { final contents = SCRoomRocketLevelContentsRes.fromJson({ 'configured': true, 'sysOrigin': 'LIKEI', 'enabled': true, 'timezone': 'Asia/Riyadh', 'maxLevel': 6, 'levels': [ { 'level': 1, 'needEnergy': 100, 'rocketIconUrl': 'icon-1', 'previewRocketUrl': 'preview-1', 'rocketAnimationUrl': 'animation-1', 'rewardPreview': { 'top1': [ { 'rewardScene': 'TOP1', 'rewardType': 'GOLD', 'rewardAmount': 100, 'rewardName': 'Lv1 top1', }, ], }, }, { 'level': 2, 'needEnergy': 200, 'rocketIconUrl': 'icon-2', 'previewRocketUrl': 'preview-2', 'rocketAnimationUrl': 'animation-2', 'rewards': [ { 'rewardScene': 'IGNITE', 'rewardType': 'GOLD', 'rewardAmount': 200, 'rewardName': 'Lv2 ignite', }, { 'rewardScene': 'IN_ROOM', 'rewardType': 'NONE', 'rewardName': '轮空', }, ], }, ], }); expect(contents.enabled, isTrue); expect(contents.levels, hasLength(2)); expect(contents.levelFor(1)?.rocketIconUrl, 'icon-1'); expect(contents.rewardPreviewForLevel(1)?.top1.single.rewardAmount, 100); expect(contents.rewardPreviewForLevel(2)?.ignite.single.rewardAmount, 200); expect(contents.rewardPreviewForLevel(2)?.inRoom, isEmpty); expect(contents.levelFor(2)?.rewards, hasLength(2)); final levels = RoomRocketApiMapper.levelsFromLevelContents(contents); final allGroups = RoomRocketApiMapper.rewardGroupsByLevelFromLevelContents( contents, ); final groups = RoomRocketApiMapper.rewardGroupsFromLevelContents( contents, level: 2, ); expect(levels?.first.imageUrl, 'icon-1'); expect(allGroups[1]?.top1.single.amount, '100'); expect(allGroups[2]?.setOff.single.amount, '200'); expect(groups?.setOff.single.amount, '200'); }); } SCRoomRocketRewardRecordRes _rewardRecord({ required String id, required String launchNo, }) { return SCRoomRocketRewardRecordRes.fromJson({ 'id': id, 'launchNo': launchNo, 'roomId': 'room-1', 'dayKey': '2026-05-15', 'roundNo': 1, 'level': 3, 'userId': 'user-1', 'userNickname': 'User 1', 'rewardScene': 'IN_ROOM', 'rewardType': 'GOLD', 'rewardItemId': 'gold', 'rewardName': 'Yumi', 'rewardAmount': 100, 'grantStatus': 'SUCCESS', 'popupStatus': 'UNREAD', }); }