//! Integration tests for Pickleball ELO Tracker use pickleball_elo::glicko::{GlickoRating, calculate_new_ratings}; use pickleball_elo::db; /// Test Glicko-2 rating calculations #[test] fn test_glicko_rating_creation() { let rating = GlickoRating::new_player(); assert_eq!(rating.rating, 1500.0); assert_eq!(rating.rd, 350.0); assert!((rating.volatility - 0.06).abs() < 0.001); } #[test] fn test_glicko_winner_gains_rating() { let winner = GlickoRating::new_player(); let loser = GlickoRating::new_player(); let (new_winner, new_loser) = calculate_new_ratings(&winner, &loser, 1.0, 1.0); // Winner should gain rating assert!(new_winner.rating > winner.rating, "Winner rating {} should be greater than {}", new_winner.rating, winner.rating); // Loser should lose rating assert!(new_loser.rating < loser.rating, "Loser rating {} should be less than {}", new_loser.rating, loser.rating); } #[test] fn test_glicko_rating_changes_are_symmetric() { let player1 = GlickoRating::new_player(); let player2 = GlickoRating::new_player(); let (new_p1, new_p2) = calculate_new_ratings(&player1, &player2, 1.0, 1.0); let p1_change = new_p1.rating - player1.rating; let p2_change = new_p2.rating - player2.rating; // Changes should be roughly symmetric (opposite signs) assert!((p1_change + p2_change).abs() < 1.0, "Rating changes should be symmetric: {} + {} = {}", p1_change, p2_change, p1_change + p2_change); } #[test] fn test_glicko_bigger_upset_bigger_change() { let favorite = GlickoRating { rating: 1800.0, rd: 100.0, volatility: 0.06 }; let underdog = GlickoRating { rating: 1400.0, rd: 100.0, volatility: 0.06 }; // Underdog wins (upset) let (new_underdog, new_favorite) = calculate_new_ratings(&underdog, &favorite, 1.0, 1.0); // Underdog should gain a lot let underdog_gain = new_underdog.rating - underdog.rating; assert!(underdog_gain > 20.0, "Underdog upset gain {} should be significant", underdog_gain); } #[test] fn test_glicko_rd_decreases_after_match() { let player1 = GlickoRating { rating: 1500.0, rd: 200.0, volatility: 0.06 }; let player2 = GlickoRating { rating: 1500.0, rd: 200.0, volatility: 0.06 }; let (new_p1, _) = calculate_new_ratings(&player1, &player2, 1.0, 1.0); // RD should decrease after playing (more certainty) assert!(new_p1.rd < player1.rd, "RD {} should decrease from {}", new_p1.rd, player1.rd); } #[test] fn test_score_weighting_blowout_vs_close() { let player1 = GlickoRating::new_player(); let player2 = GlickoRating::new_player(); // Blowout win (11-0) let (blowout_winner, _) = calculate_new_ratings(&player1, &player2, 1.0, 1.5); // Close win (11-9) let (close_winner, _) = calculate_new_ratings(&player1, &player2, 1.0, 1.05); // Blowout should give more rating assert!(blowout_winner.rating > close_winner.rating, "Blowout {} should give more than close {}", blowout_winner.rating, close_winner.rating); } /// Test database operations #[tokio::test] async fn test_database_creation() { let temp_dir = std::env::temp_dir(); let db_path = temp_dir.join("test_pickleball.db"); let db_str = db_path.to_str().unwrap(); // Clean up from previous runs let _ = std::fs::remove_file(&db_path); let pool = db::create_pool(db_str).await.expect("Failed to create pool"); // Verify tables exist let result: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM players") .fetch_one(&pool) .await .expect("Players table should exist"); assert_eq!(result.0, 0, "Players table should be empty"); // Clean up drop(pool); let _ = std::fs::remove_file(&db_path); } #[tokio::test] async fn test_player_crud() { let temp_dir = std::env::temp_dir(); let db_path = temp_dir.join("test_crud.db"); let db_str = db_path.to_str().unwrap(); let _ = std::fs::remove_file(&db_path); let pool = db::create_pool(db_str).await.unwrap(); // Create player sqlx::query("INSERT INTO players (name, email) VALUES ('Test Player', 'test@example.com')") .execute(&pool) .await .expect("Should insert player"); // Read player let player: (i64, String, Option, f64) = sqlx::query_as( "SELECT id, name, email, singles_rating FROM players WHERE name = 'Test Player'" ) .fetch_one(&pool) .await .expect("Should find player"); assert_eq!(player.1, "Test Player"); assert_eq!(player.2, Some("test@example.com".to_string())); assert_eq!(player.3, 1500.0); // Default rating // Update player sqlx::query("UPDATE players SET singles_rating = 1600.0 WHERE id = ?") .bind(player.0) .execute(&pool) .await .expect("Should update player"); let updated: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?") .bind(player.0) .fetch_one(&pool) .await .unwrap(); assert_eq!(updated.0, 1600.0); // Delete player sqlx::query("DELETE FROM players WHERE id = ?") .bind(player.0) .execute(&pool) .await .expect("Should delete player"); let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM players") .fetch_one(&pool) .await .unwrap(); assert_eq!(count.0, 0); drop(pool); let _ = std::fs::remove_file(&db_path); } #[tokio::test] async fn test_match_recording() { let temp_dir = std::env::temp_dir(); let db_path = temp_dir.join("test_matches.db"); let db_str = db_path.to_str().unwrap(); let _ = std::fs::remove_file(&db_path); let pool = db::create_pool(db_str).await.unwrap(); // Create two players sqlx::query("INSERT INTO players (name) VALUES ('Player A')") .execute(&pool).await.unwrap(); sqlx::query("INSERT INTO players (name) VALUES ('Player B')") .execute(&pool).await.unwrap(); // Create a session let session_id: i64 = sqlx::query_scalar( "INSERT INTO sessions (notes) VALUES ('Test Session') RETURNING id" ) .fetch_one(&pool) .await .unwrap(); // Create a match let match_id: i64 = sqlx::query_scalar( "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, 'singles', 11, 5) RETURNING id" ) .bind(session_id) .fetch_one(&pool) .await .unwrap(); assert!(match_id > 0, "Match should be created with valid ID"); // Verify match let match_data: (String, i32, i32) = sqlx::query_as( "SELECT match_type, team1_score, team2_score FROM matches WHERE id = ?" ) .bind(match_id) .fetch_one(&pool) .await .unwrap(); assert_eq!(match_data.0, "singles"); assert_eq!(match_data.1, 11); assert_eq!(match_data.2, 5); drop(pool); let _ = std::fs::remove_file(&db_path); } #[test] fn test_rating_bounds() { // Test that ratings don't go below 0 or above unreasonable values let very_low = GlickoRating { rating: 100.0, rd: 50.0, volatility: 0.06 }; let very_high = GlickoRating { rating: 2500.0, rd: 50.0, volatility: 0.06 }; let (new_low, _) = calculate_new_ratings(&very_low, &very_high, 0.0, 1.0); assert!(new_low.rating > 0.0, "Rating should stay positive"); assert!(new_low.rd > 0.0, "RD should stay positive"); } #[test] fn test_draw_handling() { let player1 = GlickoRating::new_player(); let player2 = GlickoRating::new_player(); // Score of 0.5 = draw let (new_p1, new_p2) = calculate_new_ratings(&player1, &player2, 0.5, 1.0); // In a draw between equal players, ratings shouldn't change much let p1_change = (new_p1.rating - player1.rating).abs(); let p2_change = (new_p2.rating - player2.rating).abs(); assert!(p1_change < 1.0, "Draw should not change rating much: {}", p1_change); assert!(p2_change < 1.0, "Draw should not change rating much: {}", p2_change); }