diff --git a/spec/services/tournament_snapshot_importer_spec.rb b/spec/services/tournament_snapshot_importer_spec.rb index 129d6f0..9668045 100644 --- a/spec/services/tournament_snapshot_importer_spec.rb +++ b/spec/services/tournament_snapshot_importer_spec.rb @@ -37,5 +37,166 @@ RSpec.describe TournamentSnapshotImporter do expect(follower.name).to eq('Newer name') expect(follower.sync_last_imported_snapshot_at).to eq(Time.zone.parse('2026-04-14T10:30:02Z')) end + + it 'round-trips large leader tournament graph with mismatched ids, many groups, deciders and 64-team playoffs' do + create_list(:tournament, 3) + source = create_large_source_tournament! + create_list(:tournament, 5) + follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret') + + snapshot = TournamentSnapshotBuilder.build(source) + + described_class.import!(tournament: follower, snapshot: snapshot) + + follower.reload + expect(follower.id).not_to eq(source.id) + expect(sync_signature(follower)).to eq(sync_signature(source)) + expect(follower.teams.count).to eq(128) + expect(follower.stages.where(level: -1).first.groups.count).to eq(32) + expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(63) + expect(follower.matches.select(&:decider?).count).to eq(source.matches.select(&:decider?).count) + expect(follower.teams.pluck(:sync_source_id)).to match_array(source.teams.pluck(:id)) + expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id)) + end + end + + def create_large_source_tournament! + tournament = create( + :tournament, + teams_count: 0, + name: 'Large Sync Source', + playoff_teams_amount: 64, + instant_finalists_amount: 64, + intermediate_round_participants_amount: 0 + ) + + group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished) + advancing_teams = [] + + 32.times do |group_index| + group = Group.create!(stage: group_stage, number: group_index + 1) + teams = 4.times.map do |seed_index| + create( + :team, + tournament: tournament, + name: "Leader-G#{group_index + 1}-S#{seed_index + 1}" + ) + end + + teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero? + teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15 + + create_group_round_robin!(group: group, teams: teams) + create_group_scores!(group: group, teams: teams) + + if group_index < 3 + create_decider_match!(group: group, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index]) + end + + advancing_teams.concat(teams.take(2)) + end + + result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: advancing_teams) + expect(result).to be_a_success + + tournament.reload + play_some_playoff_matches!(tournament) + tournament.reload + end + + def create_group_round_robin!(group:, teams:) + pairings = [ + [0, 1, 2, 1], + [2, 3, 3, 0], + [0, 2, 4, 1], + [1, 3, 2, 0], + [0, 3, 5, 2], + [1, 2, 4, 3] + ] + + pairings.each_with_index do |(left_index, right_index, left_points, right_points), position| + match = Match.create!(group: group, state: :finished, position: position, decider: false) + MatchScore.create!(match: match, team: teams[left_index], points: left_points, hidden_points: 0) + MatchScore.create!(match: match, team: teams[right_index], points: right_points, hidden_points: 0) + end + end + + def create_decider_match!(group:, teams:, hidden_points:) + match = Match.create!(group: group, state: :finished, position: 6, decider: true) + MatchScore.create!(match: match, team: teams.first, points: 0, hidden_points: hidden_points.first) + MatchScore.create!(match: match, team: teams.second, points: 0, hidden_points: hidden_points.last) + end + + def create_group_scores!(group:, teams:) + teams.each_with_index do |team, index| + GroupScore.create!( + group: group, + team: team, + group_points: 9 - index, + scored_points: 12 - index, + received_points: index, + position: index + 1 + ) + end + end + + def play_some_playoff_matches!(tournament) + initial_stage = tournament.stages.where('level >= 0').max_by(&:level) + initial_stage.matches.sort_by(&:position).first(4).each_with_index do |match, index| + match.update!(state: :finished) + ordered_scores = match.match_scores.sort_by(&:id) + ordered_scores.first.update!(points: 20 + index) + ordered_scores.second.update!(points: index) + PlayoffStageService.populate_match_below(match).uniq.each(&:save!) + end + end + + def sync_signature(tournament) + { + name: tournament.name, + public: tournament.public, + playoff_teams_amount: tournament.playoff_teams_amount, + instant_finalists_amount: tournament.instant_finalists_amount, + intermediate_round_participants_amount: tournament.intermediate_round_participants_amount, + timer_end: tournament.timer_end&.iso8601, + teams: tournament.teams.order(:name).map { |team| team.name }, + stages: tournament.stages.order(level: :asc).map do |stage| + { + level: stage.level, + state: stage.state, + groups: stage.groups.order(:number).map do |group| + { + number: group.number, + matches: group.matches.order(:position).map { |match| match_signature(match) }, + group_scores: group.group_scores.includes(:team).sort_by(&:position).map do |score| + { + team_name: score.team.name, + position: score.position, + group_points: score.group_points, + scored_points: score.scored_points, + received_points: score.received_points + } + end + } + end, + matches: stage.matches.order(:position).map { |match| match_signature(match) } + } + end + } + end + + def match_signature(match) + { + state: match.state, + position: match.position, + decider: match.decider, + scores: match.match_scores.includes(:team).sort_by { |score| score.team.name }.map do |score| + { + team_name: score.team.name, + points: score.points, + hidden_points: score.hidden_points + } + end + } end end