diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 56c9972..49d7a56 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -68,7 +68,10 @@ class Match { 'gameId': game.id, 'groupId': group?.id, 'playerIds': players.map((player) => player.id).toList(), - 'scores': scores, + 'scores': scores.map( + (playerId, scoreList) => + MapEntry(playerId, scoreList.map((score) => score.toJson()).toList()), + ), 'notes': notes, }; } diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 8ceee94..d868722 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -70,6 +70,20 @@ class DataTransferService { 'gameId': m.game.id, 'groupId': m.group?.id, 'playerIds': m.players.map((p) => p.id).toList(), + 'scores': m.scores.map( + (playerId, scores) => MapEntry( + playerId, + scores + .map( + (s) => { + 'roundNumber': s.roundNumber, + 'score': s.score, + 'change': s.change, + }, + ) + .toList(), + ), + ), 'notes': m.notes, }, ) @@ -127,126 +141,9 @@ class DataTransferService { final isValid = await _validateJsonSchema(jsonString); if (!isValid) return ImportResult.invalidSchema; - final Map decoded = - json.decode(jsonString) as Map; + final decoded = json.decode(jsonString) as Map; - final List playersJson = - (decoded['players'] as List?) ?? []; - final List gamesJson = - (decoded['games'] as List?) ?? []; - final List groupsJson = - (decoded['groups'] as List?) ?? []; - final List teamsJson = - (decoded['teams'] as List?) ?? []; - final List matchesJson = - (decoded['matches'] as List?) ?? []; - - // Import Players - final List importedPlayers = playersJson - .map((p) => Player.fromJson(p as Map)) - .toList(); - - final Map playerById = { - for (final p in importedPlayers) p.id: p, - }; - - // Import Games - final List importedGames = gamesJson - .map((g) => Game.fromJson(g as Map)) - .toList(); - - final Map gameById = { - for (final g in importedGames) g.id: g, - }; - - // Import Groups - final List importedGroups = groupsJson.map((g) { - final map = g as Map; - final memberIds = (map['memberIds'] as List? ?? []) - .cast(); - - final members = memberIds - .map((id) => playerById[id]) - .whereType() - .toList(); - - return Group( - id: map['id'] as String, - name: map['name'] as String, - description: map['description'] as String, - members: members, - createdAt: DateTime.parse(map['createdAt'] as String), - ); - }).toList(); - - final Map groupById = { - for (final g in importedGroups) g.id: g, - }; - - // Import Teams - final List importedTeams = teamsJson.map((t) { - final map = t as Map; - final memberIds = (map['memberIds'] as List? ?? []) - .cast(); - - final members = memberIds - .map((id) => playerById[id]) - .whereType() - .toList(); - - return Team( - id: map['id'] as String, - name: map['name'] as String, - members: members, - createdAt: DateTime.parse(map['createdAt'] as String), - ); - }).toList(); - - // Import Matches - final List importedMatches = matchesJson.map((m) { - final map = m as Map; - - final String gameId = map['gameId'] as String; - final String? groupId = map['groupId'] as String?; - final List playerIds = - (map['playerIds'] as List? ?? []).cast(); - final DateTime? endedAt = map['endedAt'] != null - ? DateTime.parse(map['endedAt'] as String) - : null; - - final game = gameById[gameId]; - final group = (groupId == null) ? null : groupById[groupId]; - final players = playerIds - .map((id) => playerById[id]) - .whereType() - .toList(); - - return Match( - id: map['id'] as String, - name: map['name'] as String, - game: - game ?? - Game( - name: 'Unknown', - ruleset: Ruleset.singleWinner, - description: '', - color: GameColor.blue, - icon: '', - ), - group: group, - players: players, - createdAt: DateTime.parse(map['createdAt'] as String), - endedAt: endedAt, - notes: map['notes'] as String? ?? '', - ); - }).toList(); - - // Import all data into the database - await db.playerDao.addPlayersAsList(players: importedPlayers); - await db.gameDao.addGamesAsList(games: importedGames); - await db.groupDao.addGroupsAsList(groups: importedGroups); - await db.teamDao.addTeamsAsList(teams: importedTeams); - await db.matchDao.addMatchAsList(matches: importedMatches); + await importDataToDatabase(db, decoded); return ImportResult.success; } on FormatException catch (e, stack) { @@ -262,6 +159,160 @@ class DataTransferService { } } + /// Imports parsed JSON data into the database. + @visibleForTesting + static Future importDataToDatabase( + AppDatabase db, + Map decoded, + ) async { + final importedPlayers = parsePlayersFromJson(decoded); + final playerById = {for (final p in importedPlayers) p.id: p}; + + final importedGames = parseGamesFromJson(decoded); + final gameById = {for (final g in importedGames) g.id: g}; + + final importedGroups = parseGroupsFromJson(decoded, playerById); + final groupById = {for (final g in importedGroups) g.id: g}; + + final importedTeams = parseTeamsFromJson(decoded, playerById); + + final importedMatches = parseMatchesFromJson( + decoded, + gameById, + groupById, + playerById, + ); + + await db.playerDao.addPlayersAsList(players: importedPlayers); + await db.gameDao.addGamesAsList(games: importedGames); + await db.groupDao.addGroupsAsList(groups: importedGroups); + await db.teamDao.addTeamsAsList(teams: importedTeams); + await db.matchDao.addMatchAsList(matches: importedMatches); + } + + /// Parses players from JSON data. + @visibleForTesting + static List parsePlayersFromJson(Map decoded) { + final playersJson = (decoded['players'] as List?) ?? []; + return playersJson + .map((p) => Player.fromJson(p as Map)) + .toList(); + } + + /// Parses games from JSON data. + @visibleForTesting + static List parseGamesFromJson(Map decoded) { + final gamesJson = (decoded['games'] as List?) ?? []; + return gamesJson + .map((g) => Game.fromJson(g as Map)) + .toList(); + } + + /// Parses groups from JSON data. + @visibleForTesting + static List parseGroupsFromJson( + Map decoded, + Map playerById, + ) { + final groupsJson = (decoded['groups'] as List?) ?? []; + return groupsJson.map((g) { + final map = g as Map; + final memberIds = (map['memberIds'] as List? ?? []) + .cast(); + + final members = memberIds + .map((id) => playerById[id]) + .whereType() + .toList(); + + return Group( + id: map['id'] as String, + name: map['name'] as String, + description: map['description'] as String, + members: members, + createdAt: DateTime.parse(map['createdAt'] as String), + ); + }).toList(); + } + + /// Parses teams from JSON data. + @visibleForTesting + static List parseTeamsFromJson( + Map decoded, + Map playerById, + ) { + final teamsJson = (decoded['teams'] as List?) ?? []; + return teamsJson.map((t) { + final map = t as Map; + final memberIds = (map['memberIds'] as List? ?? []) + .cast(); + + final members = memberIds + .map((id) => playerById[id]) + .whereType() + .toList(); + + return Team( + id: map['id'] as String, + name: map['name'] as String, + members: members, + createdAt: DateTime.parse(map['createdAt'] as String), + ); + }).toList(); + } + + /// Parses matches from JSON data. + @visibleForTesting + static List parseMatchesFromJson( + Map decoded, + Map gameById, + Map groupById, + Map playerById, + ) { + final matchesJson = (decoded['matches'] as List?) ?? []; + return matchesJson.map((m) { + final map = m as Map; + + final gameId = map['gameId'] as String; + final groupId = map['groupId'] as String?; + final playerIds = (map['playerIds'] as List? ?? []) + .cast(); + final endedAt = map['endedAt'] != null + ? DateTime.parse(map['endedAt'] as String) + : null; + + final game = gameById[gameId] ?? createUnknownGame(); + final group = groupId != null ? groupById[groupId] : null; + final players = playerIds + .map((id) => playerById[id]) + .whereType() + .toList(); + + return Match( + id: map['id'] as String, + name: map['name'] as String, + game: game, + group: group, + players: players, + createdAt: DateTime.parse(map['createdAt'] as String), + endedAt: endedAt, + notes: map['notes'] as String? ?? '', + ); + }).toList(); + } + + /// Creates a fallback game when the referenced game is not found. + @visibleForTesting + static Game createUnknownGame() { + return Game( + name: 'Unknown', + ruleset: Ruleset.singleWinner, + description: '', + color: GameColor.blue, + icon: '', + ); + } + /// Helper method to read file content from either bytes or path static Future _readFileContent(PlatformFile file) async { if (file.bytes != null) return utf8.decode(file.bytes!); diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart new file mode 100644 index 0000000..f75d0c0 --- /dev/null +++ b/test/services/data_transfer_service_test.dart @@ -0,0 +1,802 @@ +import 'dart:convert'; + +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; +import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score.dart'; +import 'package:tallee/data/models/team.dart'; +import 'package:tallee/services/data_transfer_service.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Game testGame; + late Group testGroup; + late Team testTeam; + late Match testMatch; + final fixedDate = DateTime(2025, 11, 19, 0, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Alice', description: 'First test player'); + testPlayer2 = Player(name: 'Bob', description: 'Second test player'); + testPlayer3 = Player(name: 'Charlie', description: 'Third player'); + + testGame = Game( + name: 'Chess', + ruleset: Ruleset.singleWinner, + description: 'Strategic board game', + color: GameColor.blue, + icon: 'chess_icon', + ); + + testGroup = Group( + name: 'Test Group', + description: 'Group for testing', + members: [testPlayer1, testPlayer2], + ); + + testTeam = Team(name: 'Test Team', members: [testPlayer1, testPlayer2]); + + testMatch = Match( + name: 'Test Match', + game: testGame, + group: testGroup, + players: [testPlayer1, testPlayer2], + notes: 'Test notes', + scores: { + testPlayer1.id: [ + Score(roundNumber: 1, score: 10, change: 10), + Score(roundNumber: 2, score: 20, change: 10), + ], + testPlayer2.id: [ + Score(roundNumber: 1, score: 15, change: 15), + Score(roundNumber: 2, score: 25, change: 10), + ], + }, + ); + }); + }); + + tearDown(() async { + await database.close(); + }); + + // Helper for getting BuildContext + Future getContext(WidgetTester tester) async { + // Minimal widget with Provider + await tester.pumpWidget( + Provider.value( + value: database, + child: MaterialApp( + home: Builder( + builder: (context) { + return Container(); + }, + ), + ), + ), + ); + final BuildContext context = tester.element(find.byType(Container)); + return context; + } + + group('DataTransferService Tests', () { + testWidgets('deleteAllData()', (tester) async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.gameDao.addGame(game: testGame); + await database.groupDao.addGroup(group: testGroup); + await database.teamDao.addTeam(team: testTeam); + await database.matchDao.addMatch(match: testMatch); + + var playerCount = await database.playerDao.getPlayerCount(); + var gameCount = await database.gameDao.getGameCount(); + var groupCount = await database.groupDao.getGroupCount(); + var teamCount = await database.teamDao.getTeamCount(); + var matchCount = await database.matchDao.getMatchCount(); + + expect(playerCount, greaterThan(0)); + expect(gameCount, greaterThan(0)); + expect(groupCount, greaterThan(0)); + expect(teamCount, greaterThan(0)); + expect(matchCount, greaterThan(0)); + + final ctx = await getContext(tester); + await DataTransferService.deleteAllData(ctx); + + playerCount = await database.playerDao.getPlayerCount(); + gameCount = await database.gameDao.getGameCount(); + groupCount = await database.groupDao.getGroupCount(); + teamCount = await database.teamDao.getTeamCount(); + matchCount = await database.matchDao.getMatchCount(); + + expect(playerCount, 0); + expect(gameCount, 0); + expect(groupCount, 0); + expect(teamCount, 0); + expect(matchCount, 0); + }); + + group('getAppDataAsJson()', () { + group('Whole export', () { + testWidgets('Exporting app data works correctly', (tester) async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); + await database.gameDao.addGame(game: testGame); + await database.groupDao.addGroup(group: testGroup); + await database.teamDao.addTeam(team: testTeam); + await database.matchDao.addMatch(match: testMatch); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + + expect(jsonString, isNotEmpty); + + final decoded = json.decode(jsonString) as Map; + + expect(decoded.containsKey('players'), true); + expect(decoded.containsKey('games'), true); + expect(decoded.containsKey('groups'), true); + expect(decoded.containsKey('teams'), true); + expect(decoded.containsKey('matches'), true); + + final players = decoded['players'] as List; + final games = decoded['games'] as List; + final groups = decoded['groups'] as List; + final teams = decoded['teams'] as List; + final matches = decoded['matches'] as List; + + expect(players.length, 2); + expect(games.length, 1); + expect(groups.length, 1); + expect(teams.length, 1); + expect(matches.length, 1); + }); + + testWidgets('Exporting empty data works correctly', (tester) async { + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + + final decoded = json.decode(jsonString) as Map; + + final players = decoded['players'] as List; + final games = decoded['games'] as List; + final groups = decoded['groups'] as List; + final teams = decoded['teams'] as List; + final matches = decoded['matches'] as List; + + expect(players, isEmpty); + expect(games, isEmpty); + expect(groups, isEmpty); + expect(teams, isEmpty); + expect(matches, isEmpty); + }); + }); + + group('Specific data', () { + testWidgets('Player data is correct', (tester) async { + await database.playerDao.addPlayer(player: testPlayer1); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final players = decoded['players'] as List; + final playerData = players[0] as Map; + + expect(playerData['id'], testPlayer1.id); + expect(playerData['name'], testPlayer1.name); + expect(playerData['description'], testPlayer1.description); + expect( + playerData['createdAt'], + testPlayer1.createdAt.toIso8601String(), + ); + }); + + testWidgets('Game data is correct', (tester) async { + await database.gameDao.addGame(game: testGame); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final games = decoded['games'] as List; + final gameData = games[0] as Map; + + expect(gameData['id'], testGame.id); + expect(gameData['name'], testGame.name); + expect(gameData['ruleset'], testGame.ruleset.name); + expect(gameData['description'], testGame.description); + expect(gameData['color'], testGame.color.name); + expect(gameData['icon'], testGame.icon); + }); + + testWidgets('Group data is correct', (tester) async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); + await database.groupDao.addGroup(group: testGroup); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final groups = decoded['groups'] as List; + final groupData = groups[0] as Map; + + expect(groupData['id'], testGroup.id); + expect(groupData['name'], testGroup.name); + expect(groupData['description'], testGroup.description); + expect(groupData['memberIds'], isA()); + + final memberIds = groupData['memberIds'] as List; + expect(memberIds.length, 2); + expect(memberIds, containsAll([testPlayer1.id, testPlayer2.id])); + }); + + testWidgets('Team data is correct', (tester) async { + await database.teamDao.addTeam(team: testTeam); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final teams = decoded['teams'] as List; + + expect(teams.length, 1); + + final teamData = teams[0] as Map; + + expect(teamData['id'], testTeam.id); + expect(teamData['name'], testTeam.name); + expect(teamData['memberIds'], isA()); + + // Note: In this system, teams don't have independent members. + // Team members are only tracked through matches via PlayerMatchTable. + // Therefore, memberIds will be empty for standalone teams. + final memberIds = teamData['memberIds'] as List; + expect(memberIds, isEmpty); + }); + + testWidgets('Match data is correct', (tester) async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2], + ); + await database.gameDao.addGame(game: testGame); + await database.groupDao.addGroup(group: testGroup); + await database.matchDao.addMatch(match: testMatch); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final matches = decoded['matches'] as List; + final matchData = matches[0] as Map; + + expect(matchData['id'], testMatch.id); + expect(matchData['name'], testMatch.name); + expect(matchData['gameId'], testGame.id); + expect(matchData['groupId'], testGroup.id); + expect(matchData['playerIds'], isA()); + expect(matchData['notes'], testMatch.notes); + + // Check player ids + final playerIds = matchData['playerIds'] as List; + expect(playerIds.length, 2); + expect(playerIds, containsAll([testPlayer1.id, testPlayer2.id])); + + // Check scores structure + final scoresJson = matchData['scores'] as Map; + expect(scoresJson, isA>()); + + final scores = scoresJson.map( + (playerId, scoreList) => MapEntry( + playerId, + (scoreList as List) + .map((s) => Score.fromJson(s as Map)) + .toList(), + ), + ); + + expect(scores, isA>>()); + + /* Player 1 scores */ + // General structure + expect(scores[testPlayer1.id], isNotNull); + expect(scores[testPlayer1.id]!.length, 2); + + // Round 1 + expect(scores[testPlayer1.id]![0].roundNumber, 1); + expect(scores[testPlayer1.id]![0].score, 10); + expect(scores[testPlayer1.id]![0].change, 10); + + // Round 2 + expect(scores[testPlayer1.id]![1].roundNumber, 2); + expect(scores[testPlayer1.id]![1].score, 20); + expect(scores[testPlayer1.id]![1].change, 10); + + /* Player 2 scores */ + // General structure + expect(scores[testPlayer2.id], isNotNull); + expect(scores[testPlayer2.id]!.length, 2); + + // Round 1 + expect(scores[testPlayer2.id]![0].roundNumber, 1); + expect(scores[testPlayer2.id]![0].score, 15); + expect(scores[testPlayer2.id]![0].change, 15); + + // Round 2 + expect(scores[testPlayer2.id]![1].roundNumber, 2); + expect(scores[testPlayer2.id]![1].score, 25); + expect(scores[testPlayer2.id]![1].change, 10); + }); + + testWidgets('Match without group is handled correctly', (tester) async { + final matchWithoutGroup = Match( + name: 'No Group Match', + game: testGame, + group: null, + players: [testPlayer1], + notes: 'No group', + ); + + await database.playerDao.addPlayer(player: testPlayer1); + await database.gameDao.addGame(game: testGame); + await database.matchDao.addMatch(match: matchWithoutGroup); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final matches = decoded['matches'] as List; + final matchData = matches[0] as Map; + + expect(matchData['groupId'], isNull); + }); + + testWidgets('Match with endedAt is handled correctly', (tester) async { + final endedDate = DateTime(2025, 12, 1, 10, 0, 0); + final endedMatch = Match( + name: 'Ended Match', + game: testGame, + players: [testPlayer1], + endedAt: endedDate, + notes: 'Finished', + ); + + await database.playerDao.addPlayer(player: testPlayer1); + await database.gameDao.addGame(game: testGame); + await database.matchDao.addMatch(match: endedMatch); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final matches = decoded['matches'] as List; + final matchData = matches[0] as Map; + + expect(matchData['endedAt'], endedDate.toIso8601String()); + }); + + testWidgets('Structure is consistent', (tester) async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.gameDao.addGame(game: testGame); + + final ctx = await getContext(tester); + final jsonString1 = await DataTransferService.getAppDataAsJson(ctx); + final jsonString2 = await DataTransferService.getAppDataAsJson(ctx); + + expect(jsonString1, equals(jsonString2)); + }); + + testWidgets('Empty match notes is handled correctly', (tester) async { + final matchWithEmptyNotes = Match( + name: 'Empty Notes Match', + game: testGame, + players: [testPlayer1], + notes: '', + ); + + await database.playerDao.addPlayer(player: testPlayer1); + await database.gameDao.addGame(game: testGame); + await database.matchDao.addMatch(match: matchWithEmptyNotes); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final matches = decoded['matches'] as List; + final matchData = matches[0] as Map; + + expect(matchData['notes'], ''); + }); + + testWidgets('Multiple players in match is handled correctly', ( + tester, + ) async { + final multiPlayerMatch = Match( + name: 'Multi Player Match', + game: testGame, + players: [testPlayer1, testPlayer2, testPlayer3], + notes: 'Three players', + ); + + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2, testPlayer3], + ); + await database.gameDao.addGame(game: testGame); + await database.matchDao.addMatch(match: multiPlayerMatch); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final matches = decoded['matches'] as List; + final matchData = matches[0] as Map; + + final playerIds = matchData['playerIds'] as List; + expect(playerIds.length, 3); + expect( + playerIds, + containsAll([testPlayer1.id, testPlayer2.id, testPlayer3.id]), + ); + }); + + testWidgets('All game colors are handled correctly', (tester) async { + final games = [ + Game( + name: 'Red Game', + ruleset: Ruleset.singleWinner, + color: GameColor.red, + icon: 'icon', + ), + Game( + name: 'Blue Game', + ruleset: Ruleset.singleWinner, + color: GameColor.blue, + icon: 'icon', + ), + Game( + name: 'Green Game', + ruleset: Ruleset.singleWinner, + color: GameColor.green, + icon: 'icon', + ), + ]; + + await database.gameDao.addGamesAsList(games: games); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final gamesJson = decoded['games'] as List; + + expect(gamesJson.length, 3); + expect( + gamesJson.map((g) => g['color']), + containsAll(['red', 'blue', 'green']), + ); + }); + + testWidgets('All rulesets are handled correctly', (tester) async { + final games = [ + Game( + name: 'Highest Score Game', + ruleset: Ruleset.highestScore, + color: GameColor.blue, + icon: 'icon', + ), + Game( + name: 'Lowest Score Game', + ruleset: Ruleset.lowestScore, + color: GameColor.blue, + icon: 'icon', + ), + Game( + name: 'Single Winner', + ruleset: Ruleset.singleWinner, + color: GameColor.blue, + icon: 'icon', + ), + ]; + + await database.gameDao.addGamesAsList(games: games); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + final decoded = json.decode(jsonString) as Map; + final gamesJson = decoded['games'] as List; + + expect(gamesJson.length, 3); + expect( + gamesJson.map((g) => g['ruleset']), + containsAll(['highestScore', 'lowestScore', 'singleWinner']), + ); + }); + }); + }); + + group('Parse Methods', () { + test('parsePlayersFromJson()', () { + final jsonMap = { + 'players': [ + { + 'id': testPlayer1.id, + 'name': testPlayer1.name, + 'description': testPlayer1.description, + 'createdAt': testPlayer1.createdAt.toIso8601String(), + }, + { + 'id': testPlayer2.id, + 'name': testPlayer2.name, + 'description': testPlayer2.description, + 'createdAt': testPlayer2.createdAt.toIso8601String(), + }, + ], + }; + + final players = DataTransferService.parsePlayersFromJson(jsonMap); + + expect(players.length, 2); + expect(players[0].id, testPlayer1.id); + expect(players[0].name, testPlayer1.name); + expect(players[1].id, testPlayer2.id); + expect(players[1].name, testPlayer2.name); + }); + + test('parsePlayersFromJson() empty list', () { + final jsonMap = {'players': []}; + final players = DataTransferService.parsePlayersFromJson(jsonMap); + expect(players, isEmpty); + }); + + test('parsePlayersFromJson() missing key', () { + final jsonMap = {}; + final players = DataTransferService.parsePlayersFromJson(jsonMap); + expect(players, isEmpty); + }); + + test('parseGamesFromJson()', () { + final jsonMap = { + 'games': [ + { + 'id': testGame.id, + 'name': testGame.name, + 'ruleset': testGame.ruleset.name, + 'description': testGame.description, + 'color': testGame.color.name, + 'icon': testGame.icon, + 'createdAt': testGame.createdAt.toIso8601String(), + }, + ], + }; + + final games = DataTransferService.parseGamesFromJson(jsonMap); + + expect(games.length, 1); + expect(games[0].id, testGame.id); + expect(games[0].name, testGame.name); + expect(games[0].ruleset, testGame.ruleset); + }); + + test('parseGroupsFromJson()', () { + final playerById = { + testPlayer1.id: testPlayer1, + testPlayer2.id: testPlayer2, + }; + + final jsonMap = { + 'groups': [ + { + 'id': testGroup.id, + 'name': testGroup.name, + 'description': testGroup.description, + 'memberIds': [testPlayer1.id, testPlayer2.id], + 'createdAt': testGroup.createdAt.toIso8601String(), + }, + ], + }; + + final groups = DataTransferService.parseGroupsFromJson( + jsonMap, + playerById, + ); + + expect(groups.length, 1); + expect(groups[0].id, testGroup.id); + expect(groups[0].name, testGroup.name); + expect(groups[0].members.length, 2); + expect(groups[0].members[0].id, testPlayer1.id); + expect(groups[0].members[1].id, testPlayer2.id); + }); + + test('parseGroupsFromJson() ignores invalid player ids', () { + final playerById = {testPlayer1.id: testPlayer1}; + + final jsonMap = { + 'groups': [ + { + 'id': testGroup.id, + 'name': testGroup.name, + 'description': testGroup.description, + 'memberIds': [testPlayer1.id, 'invalid-id'], + 'createdAt': testGroup.createdAt.toIso8601String(), + }, + ], + }; + + final groups = DataTransferService.parseGroupsFromJson( + jsonMap, + playerById, + ); + + expect(groups.length, 1); + expect(groups[0].members.length, 1); + expect(groups[0].members[0].id, testPlayer1.id); + }); + + test('parseTeamsFromJson()', () { + final playerById = {testPlayer1.id: testPlayer1}; + + final jsonMap = { + 'teams': [ + { + 'id': testTeam.id, + 'name': testTeam.name, + 'memberIds': [testPlayer1.id], + 'createdAt': testTeam.createdAt.toIso8601String(), + }, + ], + }; + + final teams = DataTransferService.parseTeamsFromJson( + jsonMap, + playerById, + ); + + expect(teams.length, 1); + expect(teams[0].id, testTeam.id); + expect(teams[0].name, testTeam.name); + expect(teams[0].members.length, 1); + expect(teams[0].members[0].id, testPlayer1.id); + }); + + test('parseMatchesFromJson()', () { + final playerById = { + testPlayer1.id: testPlayer1, + testPlayer2.id: testPlayer2, + }; + final gameById = {testGame.id: testGame}; + final groupById = {testGroup.id: testGroup}; + + final jsonMap = { + 'matches': [ + { + 'id': testMatch.id, + 'name': testMatch.name, + 'gameId': testGame.id, + 'groupId': testGroup.id, + 'playerIds': [testPlayer1.id, testPlayer2.id], + 'notes': testMatch.notes, + 'createdAt': testMatch.createdAt.toIso8601String(), + }, + ], + }; + + final matches = DataTransferService.parseMatchesFromJson( + jsonMap, + gameById, + groupById, + playerById, + ); + + expect(matches.length, 1); + expect(matches[0].id, testMatch.id); + expect(matches[0].name, testMatch.name); + expect(matches[0].game.id, testGame.id); + expect(matches[0].group?.id, testGroup.id); + expect(matches[0].players.length, 2); + }); + + test('parseMatchesFromJson() creates unknown game for missing game', () { + final playerById = {testPlayer1.id: testPlayer1}; + final gameById = {}; + final groupById = {}; + + final jsonMap = { + 'matches': [ + { + 'id': testMatch.id, + 'name': testMatch.name, + 'gameId': 'non-existent-game-id', + 'playerIds': [testPlayer1.id], + 'notes': '', + 'createdAt': testMatch.createdAt.toIso8601String(), + }, + ], + }; + + final matches = DataTransferService.parseMatchesFromJson( + jsonMap, + gameById, + groupById, + playerById, + ); + + expect(matches.length, 1); + expect(matches[0].game.name, 'Unknown'); + expect(matches[0].game.ruleset, Ruleset.singleWinner); + }); + + test('parseMatchesFromJson() handles null group', () { + final playerById = {testPlayer1.id: testPlayer1}; + final gameById = {testGame.id: testGame}; + final groupById = {}; + + final jsonMap = { + 'matches': [ + { + 'id': testMatch.id, + 'name': testMatch.name, + 'gameId': testGame.id, + 'groupId': null, + 'playerIds': [testPlayer1.id], + 'notes': '', + 'createdAt': testMatch.createdAt.toIso8601String(), + }, + ], + }; + + final matches = DataTransferService.parseMatchesFromJson( + jsonMap, + gameById, + groupById, + playerById, + ); + + expect(matches.length, 1); + expect(matches[0].group, isNull); + }); + + test('parseMatchesFromJson() handles endedAt', () { + final playerById = {testPlayer1.id: testPlayer1}; + final gameById = {testGame.id: testGame}; + final groupById = {}; + final endedDate = DateTime(2025, 12, 1, 10, 0, 0); + + final jsonMap = { + 'matches': [ + { + 'id': testMatch.id, + 'name': testMatch.name, + 'gameId': testGame.id, + 'playerIds': [testPlayer1.id], + 'notes': '', + 'createdAt': testMatch.createdAt.toIso8601String(), + 'endedAt': endedDate.toIso8601String(), + }, + ], + }; + + final matches = DataTransferService.parseMatchesFromJson( + jsonMap, + gameById, + groupById, + playerById, + ); + + expect(matches.length, 1); + expect(matches[0].endedAt, endedDate); + }); + }); + }); +}