//! Match reversal/deletion tests //! //! Ensures that deleting matches correctly reverts ratings use sqlx::SqlitePool; use sqlx::sqlite::SqlitePoolOptions; async fn setup_test_db() -> SqlitePool { let pool = SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .expect("Failed to create test database"); sqlx::query(r#" CREATE TABLE players ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, email TEXT DEFAULT '', singles_rating REAL DEFAULT 1500.0, singles_rd REAL DEFAULT 350.0, doubles_rating REAL DEFAULT 1500.0, doubles_rd REAL DEFAULT 350.0, created_at TEXT DEFAULT (datetime('now')) ) "#).execute(&pool).await.unwrap(); sqlx::query(r#" CREATE TABLE matches ( id INTEGER PRIMARY KEY AUTOINCREMENT, match_type TEXT NOT NULL, team1_score INTEGER NOT NULL, team2_score INTEGER NOT NULL, timestamp TEXT DEFAULT (datetime('now')), session_id INTEGER ) "#).execute(&pool).await.unwrap(); sqlx::query(r#" CREATE TABLE match_participants ( id INTEGER PRIMARY KEY AUTOINCREMENT, match_id INTEGER NOT NULL, player_id INTEGER NOT NULL, team INTEGER NOT NULL, rating_before REAL, rating_after REAL, rating_change REAL, FOREIGN KEY (match_id) REFERENCES matches(id), FOREIGN KEY (player_id) REFERENCES players(id) ) "#).execute(&pool).await.unwrap(); pool } #[tokio::test] async fn test_singles_match_reversal() { let pool = setup_test_db().await; // Create two players sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind("Reversal A") .bind(1500.0) .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind("Reversal B") .bind(1500.0) .execute(&pool) .await .unwrap(); let pa_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Reversal A") .fetch_one(&pool) .await .unwrap(); let pb_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Reversal B") .fetch_one(&pool) .await .unwrap(); // Record match with rating changes let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)" ) .bind("singles") .bind(11) .bind(5) .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); let rating_change_a = 10.0; let rating_change_b = -10.0; // Record participants with rating changes sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) VALUES (?, ?, ?, ?, ?, ?)" ) .bind(match_id) .bind(pa_id.0) .bind(1) .bind(1500.0) .bind(1510.0) .bind(rating_change_a) .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) VALUES (?, ?, ?, ?, ?, ?)" ) .bind(match_id) .bind(pb_id.0) .bind(2) .bind(1500.0) .bind(1490.0) .bind(rating_change_b) .execute(&pool) .await .unwrap(); // Apply rating changes sqlx::query("UPDATE players SET singles_rating = singles_rating + ? WHERE id = ?") .bind(rating_change_a) .bind(pa_id.0) .execute(&pool) .await .unwrap(); sqlx::query("UPDATE players SET singles_rating = singles_rating + ? WHERE id = ?") .bind(rating_change_b) .bind(pb_id.0) .execute(&pool) .await .unwrap(); // Verify ratings changed let rating_a: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pa_id.0) .fetch_one(&pool) .await .unwrap(); assert!((rating_a.0 - 1510.0).abs() < 0.01); let rating_b: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pb_id.0) .fetch_one(&pool) .await .unwrap(); assert!((rating_b.0 - 1490.0).abs() < 0.01); // NOW REVERSE THE MATCH // Get rating changes to revert let participants: Vec<(i64, f64)> = sqlx::query_as( "SELECT player_id, rating_change FROM match_participants WHERE match_id = ?" ) .bind(match_id) .fetch_all(&pool) .await .unwrap(); // Revert each player's rating for (player_id, change) in participants { sqlx::query("UPDATE players SET singles_rating = singles_rating - ? WHERE id = ?") .bind(change) .bind(player_id) .execute(&pool) .await .unwrap(); } // Delete match participants and match sqlx::query("DELETE FROM match_participants WHERE match_id = ?") .bind(match_id) .execute(&pool) .await .unwrap(); sqlx::query("DELETE FROM matches WHERE id = ?") .bind(match_id) .execute(&pool) .await .unwrap(); // Verify ratings are back to original let final_rating_a: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pa_id.0) .fetch_one(&pool) .await .unwrap(); assert!((final_rating_a.0 - 1500.0).abs() < 0.01, "Player A rating not reverted: {}", final_rating_a.0); let final_rating_b: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pb_id.0) .fetch_one(&pool) .await .unwrap(); assert!((final_rating_b.0 - 1500.0).abs() < 0.01, "Player B rating not reverted: {}", final_rating_b.0); } #[tokio::test] async fn test_doubles_match_reversal() { let pool = setup_test_db().await; // Create four players let players = [ ("D1", 1500.0), ("D2", 1520.0), ("D3", 1480.0), ("D4", 1500.0) ]; for (name, rating) in &players { sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind(*name) .bind(*rating) .execute(&pool) .await .unwrap(); } let d1_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = 'D1'") .fetch_one(&pool).await.unwrap(); let d2_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = 'D2'") .fetch_one(&pool).await.unwrap(); let d3_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = 'D3'") .fetch_one(&pool).await.unwrap(); let d4_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = 'D4'") .fetch_one(&pool).await.unwrap(); let d1_id = d1_id.0; let d2_id = d2_id.0; let d3_id = d3_id.0; let d4_id = d4_id.0; // Record doubles match let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)" ) .bind("doubles") .bind(11) .bind(8) .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); // Team 1 (D1 + D2) wins, Team 2 (D3 + D4) loses // Each player has different rating change due to effective opponent let changes = [ (d1_id, 1, 8.0), (d2_id, 1, 5.0), // Stronger player gains less (d3_id, 2, -7.0), (d4_id, 2, -6.0), ]; for (player_id, team, change) in &changes { let before = match *team { 1 => if *player_id == d1_id { 1500.0 } else { 1520.0 }, _ => if *player_id == d3_id { 1480.0 } else { 1500.0 }, }; sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) VALUES (?, ?, ?, ?, ?, ?)" ) .bind(match_id) .bind(player_id) .bind(team) .bind(before) .bind(before + change) .bind(change) .execute(&pool) .await .unwrap(); // Apply rating change sqlx::query("UPDATE players SET singles_rating = singles_rating + ? WHERE id = ?") .bind(change) .bind(player_id) .execute(&pool) .await .unwrap(); } // Store original ratings for comparison let original_ratings: Vec<(i64, f64)> = vec![ (d1_id, 1500.0), (d2_id, 1520.0), (d3_id, 1480.0), (d4_id, 1500.0) ]; // REVERSE THE MATCH let participants: Vec<(i64, f64)> = sqlx::query_as( "SELECT player_id, rating_change FROM match_participants WHERE match_id = ?" ) .bind(match_id) .fetch_all(&pool) .await .unwrap(); for (player_id, change) in participants { sqlx::query("UPDATE players SET singles_rating = singles_rating - ? WHERE id = ?") .bind(change) .bind(player_id) .execute(&pool) .await .unwrap(); } sqlx::query("DELETE FROM match_participants WHERE match_id = ?") .bind(match_id) .execute(&pool) .await .unwrap(); sqlx::query("DELETE FROM matches WHERE id = ?") .bind(match_id) .execute(&pool) .await .unwrap(); // Verify all ratings reverted for (player_id, original_rating) in original_ratings { let final_rating: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(player_id) .fetch_one(&pool) .await .unwrap(); assert!((final_rating.0 - original_rating).abs() < 0.01, "Player {} rating not reverted: {} (expected {})", player_id, final_rating.0, original_rating); } } #[tokio::test] async fn test_multiple_match_reversal() { let pool = setup_test_db().await; sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind("Multi A") .bind(1500.0) .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind("Multi B") .bind(1500.0) .execute(&pool) .await .unwrap(); let pa_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Multi A") .fetch_one(&pool) .await .unwrap(); let pb_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Multi B") .fetch_one(&pool) .await .unwrap(); // Record 5 matches let mut match_ids = vec![]; let mut changes_a = vec![]; let mut changes_b = vec![]; for i in 0..5 { let score1 = 11; let score2 = 5 + i; let change_a = 10.0 - i as f64; let change_b = -(10.0 - i as f64); let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)" ) .bind("singles") .bind(score1) .bind(score2) .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); match_ids.push(match_id); changes_a.push(change_a); changes_b.push(change_b); // Record participants sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)" ) .bind(match_id) .bind(pa_id.0) .bind(1) .bind(change_a) .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)" ) .bind(match_id) .bind(pb_id.0) .bind(2) .bind(change_b) .execute(&pool) .await .unwrap(); // Apply changes sqlx::query("UPDATE players SET singles_rating = singles_rating + ? WHERE id = ?") .bind(change_a) .bind(pa_id.0) .execute(&pool) .await .unwrap(); sqlx::query("UPDATE players SET singles_rating = singles_rating + ? WHERE id = ?") .bind(change_b) .bind(pb_id.0) .execute(&pool) .await .unwrap(); } // Verify ratings after 5 matches // A gains: 10+9+8+7+6 = 40 // B loses: -40 let rating_a: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pa_id.0) .fetch_one(&pool) .await .unwrap(); assert!((rating_a.0 - 1540.0).abs() < 0.01); // Delete matches in REVERSE order (LIFO - last in, first out) for match_id in match_ids.iter().rev() { let participants: Vec<(i64, f64)> = sqlx::query_as( "SELECT player_id, rating_change FROM match_participants WHERE match_id = ?" ) .bind(match_id) .fetch_all(&pool) .await .unwrap(); for (player_id, change) in participants { sqlx::query("UPDATE players SET singles_rating = singles_rating - ? WHERE id = ?") .bind(change) .bind(player_id) .execute(&pool) .await .unwrap(); } sqlx::query("DELETE FROM match_participants WHERE match_id = ?") .bind(match_id) .execute(&pool) .await .unwrap(); sqlx::query("DELETE FROM matches WHERE id = ?") .bind(match_id) .execute(&pool) .await .unwrap(); } // Verify both players back to 1500 let final_a: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pa_id.0) .fetch_one(&pool) .await .unwrap(); assert!((final_a.0 - 1500.0).abs() < 0.01); let final_b: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pb_id.0) .fetch_one(&pool) .await .unwrap(); assert!((final_b.0 - 1500.0).abs() < 0.01); } #[tokio::test] async fn test_reversal_preserves_other_matches() { let pool = setup_test_db().await; // Create 3 players: A plays both B and C for (name, rating) in [("Preserve A", 1500.0), ("Preserve B", 1500.0), ("Preserve C", 1500.0)] { sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind(name) .bind(rating) .execute(&pool) .await .unwrap(); } let pa_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Preserve A") .fetch_one(&pool) .await .unwrap(); let pb_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Preserve B") .fetch_one(&pool) .await .unwrap(); let pc_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Preserve C") .fetch_one(&pool) .await .unwrap(); // Match 1: A vs B (A wins) let match1 = sqlx::query("INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)") .bind("singles").bind(11).bind(5) .execute(&pool).await.unwrap(); let match1_id = match1.last_insert_rowid(); sqlx::query("INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)") .bind(match1_id).bind(pa_id.0).bind(1).bind(10.0) .execute(&pool).await.unwrap(); sqlx::query("INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)") .bind(match1_id).bind(pb_id.0).bind(2).bind(-10.0) .execute(&pool).await.unwrap(); sqlx::query("UPDATE players SET singles_rating = singles_rating + 10 WHERE id = ?") .bind(pa_id.0).execute(&pool).await.unwrap(); sqlx::query("UPDATE players SET singles_rating = singles_rating - 10 WHERE id = ?") .bind(pb_id.0).execute(&pool).await.unwrap(); // Match 2: A vs C (A wins) let match2 = sqlx::query("INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)") .bind("singles").bind(11).bind(7) .execute(&pool).await.unwrap(); let match2_id = match2.last_insert_rowid(); sqlx::query("INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)") .bind(match2_id).bind(pa_id.0).bind(1).bind(8.0) .execute(&pool).await.unwrap(); sqlx::query("INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)") .bind(match2_id).bind(pc_id.0).bind(2).bind(-8.0) .execute(&pool).await.unwrap(); sqlx::query("UPDATE players SET singles_rating = singles_rating + 8 WHERE id = ?") .bind(pa_id.0).execute(&pool).await.unwrap(); sqlx::query("UPDATE players SET singles_rating = singles_rating - 8 WHERE id = ?") .bind(pc_id.0).execute(&pool).await.unwrap(); // After both matches: A=1518, B=1490, C=1492 // Delete ONLY match 1 (A vs B) let participants: Vec<(i64, f64)> = sqlx::query_as( "SELECT player_id, rating_change FROM match_participants WHERE match_id = ?" ) .bind(match1_id) .fetch_all(&pool) .await .unwrap(); for (player_id, change) in participants { sqlx::query("UPDATE players SET singles_rating = singles_rating - ? WHERE id = ?") .bind(change) .bind(player_id) .execute(&pool) .await .unwrap(); } sqlx::query("DELETE FROM match_participants WHERE match_id = ?") .bind(match1_id) .execute(&pool) .await .unwrap(); sqlx::query("DELETE FROM matches WHERE id = ?") .bind(match1_id) .execute(&pool) .await .unwrap(); // After reverting match 1: A=1508, B=1500, C=1492 // Match 2's effect on A and C should be preserved let final_a: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pa_id.0) .fetch_one(&pool) .await .unwrap(); assert!((final_a.0 - 1508.0).abs() < 0.01, "A rating wrong: {}", final_a.0); let final_b: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pb_id.0) .fetch_one(&pool) .await .unwrap(); assert!((final_b.0 - 1500.0).abs() < 0.01, "B rating wrong: {}", final_b.0); let final_c: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(pc_id.0) .fetch_one(&pool) .await .unwrap(); assert!((final_c.0 - 1492.0).abs() < 0.01, "C rating wrong: {}", final_c.0); // Match 2 should still exist let match_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM matches") .fetch_one(&pool) .await .unwrap(); assert_eq!(match_count.0, 1); }