use std::collections::HashMap; use sba_scout::db::{queries, schema}; /// Create an in-memory SQLite pool with all tables initialized. async fn test_pool() -> sqlx::SqlitePool { let pool = schema::init_pool(std::path::Path::new(":memory:")) .await .expect("failed to create in-memory pool"); schema::create_tables(&pool) .await .expect("failed to create tables"); pool } /// Insert a test team and return its id. async fn insert_team(pool: &sqlx::SqlitePool, id: i64, abbrev: &str, season: i64) { sqlx::query( "INSERT INTO teams (id, abbrev, short_name, long_name, season) VALUES (?, ?, ?, ?, ?)", ) .bind(id) .bind(abbrev) .bind(abbrev) .bind(format!("{} Long Name", abbrev)) .bind(season) .execute(pool) .await .expect("failed to insert team"); } /// Insert a test player and return its id. async fn insert_player( pool: &sqlx::SqlitePool, id: i64, name: &str, team_id: i64, season: i64, pos_1: &str, ) { sqlx::query( "INSERT INTO players (id, name, season, team_id, pos_1) VALUES (?, ?, ?, ?, ?)", ) .bind(id) .bind(name) .bind(season) .bind(team_id) .bind(pos_1) .execute(pool) .await .expect("failed to insert player"); } // ============================================================================= // Team Query Tests // ============================================================================= #[tokio::test] async fn get_all_teams_returns_teams_for_season() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_team(&pool, 2, "NYM", 13).await; insert_team(&pool, 3, "WV", 12).await; // different season let teams = queries::get_all_teams(&pool, 13, false).await.unwrap(); assert_eq!(teams.len(), 2); } #[tokio::test] async fn get_all_teams_active_only_excludes_il_and_mil() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_team(&pool, 2, "WVIL", 13).await; insert_team(&pool, 3, "WVMiL", 13).await; let teams = queries::get_all_teams(&pool, 13, true).await.unwrap(); assert_eq!(teams.len(), 1); assert_eq!(teams[0].abbrev, "WV"); } #[tokio::test] async fn get_team_by_abbrev_finds_correct_team() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_team(&pool, 2, "WV", 12).await; let team = queries::get_team_by_abbrev(&pool, "WV", 13).await.unwrap(); assert!(team.is_some()); assert_eq!(team.unwrap().id, 1); } #[tokio::test] async fn get_team_by_abbrev_returns_none_when_missing() { let pool = test_pool().await; let team = queries::get_team_by_abbrev(&pool, "NOPE", 13).await.unwrap(); assert!(team.is_none()); } #[tokio::test] async fn get_team_by_id_works() { let pool = test_pool().await; insert_team(&pool, 42, "ATL", 13).await; let team = queries::get_team_by_id(&pool, 42).await.unwrap(); assert!(team.is_some()); assert_eq!(team.unwrap().abbrev, "ATL"); } // ============================================================================= // Player Query Tests // ============================================================================= #[tokio::test] async fn get_players_by_team_returns_sorted_by_name() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Zack Wheeler", 1, 13, "SP").await; insert_player(&pool, 11, "Aaron Nola", 1, 13, "SP").await; let players = queries::get_players_by_team(&pool, 1).await.unwrap(); assert_eq!(players.len(), 2); assert_eq!(players[0].name, "Aaron Nola"); assert_eq!(players[1].name, "Zack Wheeler"); } #[tokio::test] async fn get_player_by_name_is_case_insensitive() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Mike Trout", 1, 13, "CF").await; let player = queries::get_player_by_name(&pool, "mike trout", 13) .await .unwrap(); assert!(player.is_some()); assert_eq!(player.unwrap().id, 10); } #[tokio::test] async fn search_players_uses_like_pattern() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Mike Trout", 1, 13, "CF").await; insert_player(&pool, 11, "Mike Piazza", 1, 13, "C").await; insert_player(&pool, 12, "Derek Jeter", 1, 13, "SS").await; let results = queries::search_players(&pool, "Mike", 13, 10).await.unwrap(); assert_eq!(results.len(), 2); } #[tokio::test] async fn search_players_respects_limit() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Mike Trout", 1, 13, "CF").await; insert_player(&pool, 11, "Mike Piazza", 1, 13, "C").await; let results = queries::search_players(&pool, "Mike", 13, 1).await.unwrap(); assert_eq!(results.len(), 1); } #[tokio::test] async fn get_pitchers_filters_by_position() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Zack Wheeler", 1, 13, "SP").await; insert_player(&pool, 11, "Mike Trout", 1, 13, "CF").await; let pitchers = queries::get_pitchers(&pool, Some(1), Some(13)).await.unwrap(); assert_eq!(pitchers.len(), 1); assert_eq!(pitchers[0].name, "Zack Wheeler"); } #[tokio::test] async fn get_batters_filters_by_position() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Zack Wheeler", 1, 13, "SP").await; insert_player(&pool, 11, "Mike Trout", 1, 13, "CF").await; let batters = queries::get_batters(&pool, Some(1), Some(13)).await.unwrap(); assert_eq!(batters.len(), 1); assert_eq!(batters[0].name, "Mike Trout"); } #[tokio::test] async fn get_players_missing_batter_cards() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Mike Trout", 1, 13, "CF").await; insert_player(&pool, 11, "Mookie Betts", 1, 13, "RF").await; // Give Trout a batter card sqlx::query("INSERT INTO batter_cards (player_id) VALUES (?)") .bind(10) .execute(&pool) .await .unwrap(); let missing = queries::get_players_missing_cards(&pool, 13, "batter") .await .unwrap(); assert_eq!(missing.len(), 1); assert_eq!(missing[0].name, "Mookie Betts"); } // ============================================================================= // Card Query Tests // ============================================================================= #[tokio::test] async fn get_batter_card_returns_none_when_missing() { let pool = test_pool().await; let card = queries::get_batter_card(&pool, 999).await.unwrap(); assert!(card.is_none()); } #[tokio::test] async fn get_pitcher_card_returns_none_when_missing() { let pool = test_pool().await; let card = queries::get_pitcher_card(&pool, 999).await.unwrap(); assert!(card.is_none()); } // ============================================================================= // Sync Status Tests // ============================================================================= #[tokio::test] async fn sync_status_upsert_creates_and_updates() { let pool = test_pool().await; // First call creates queries::update_sync_status(&pool, "teams", 25, None) .await .unwrap(); let status = queries::get_sync_status(&pool, "teams").await.unwrap(); assert!(status.is_some()); assert_eq!(status.unwrap().last_sync_count, Some(25)); // Second call updates (upsert) queries::update_sync_status(&pool, "teams", 30, Some("timeout")) .await .unwrap(); let status = queries::get_sync_status(&pool, "teams").await.unwrap(); let s = status.unwrap(); assert_eq!(s.last_sync_count, Some(30)); assert_eq!(s.last_error.as_deref(), Some("timeout")); } // ============================================================================= // Matchup Cache Tests // ============================================================================= #[tokio::test] async fn matchup_cache_returns_none_when_empty() { let pool = test_pool().await; let cache = queries::get_cached_matchup(&pool, 1, 2, "abc123") .await .unwrap(); assert!(cache.is_none()); } #[tokio::test] async fn invalidate_matchup_cache_returns_count() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_player(&pool, 10, "Batter", 1, 13, "CF").await; insert_player(&pool, 11, "Pitcher", 1, 13, "SP").await; sqlx::query( "INSERT INTO matchup_cache (batter_id, pitcher_id, rating, weights_hash) VALUES (?, ?, ?, ?)", ) .bind(10) .bind(11) .bind(75.5) .bind("hash1") .execute(&pool) .await .unwrap(); let deleted = queries::invalidate_matchup_cache(&pool).await.unwrap(); assert_eq!(deleted, 1); } // ============================================================================= // Lineup CRUD Tests // ============================================================================= #[tokio::test] async fn save_and_get_lineup() { let pool = test_pool().await; let order = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; let mut positions = HashMap::new(); positions.insert("SS".to_string(), 1); positions.insert("CF".to_string(), 2); queries::save_lineup(&pool, "vs LHP", &order, &positions, "standard", Some("Test lineup"), None) .await .unwrap(); let lineup = queries::get_lineup_by_name(&pool, "vs LHP").await.unwrap(); assert!(lineup.is_some()); let l = lineup.unwrap(); assert_eq!(l.name, "vs LHP"); assert_eq!(l.batting_order_vec(), order); assert_eq!(l.positions_map(), positions); } #[tokio::test] async fn save_lineup_updates_existing() { let pool = test_pool().await; let positions = HashMap::new(); queries::save_lineup(&pool, "lineup1", &[1, 2, 3], &positions, "standard", None, None) .await .unwrap(); queries::save_lineup(&pool, "lineup1", &[4, 5, 6], &positions, "standard", None, None) .await .unwrap(); let lineups = queries::get_lineups(&pool).await.unwrap(); assert_eq!(lineups.len(), 1, "should update, not duplicate"); assert_eq!(lineups[0].batting_order_vec(), vec![4, 5, 6]); } #[tokio::test] async fn delete_lineup_returns_true_when_found() { let pool = test_pool().await; let positions = HashMap::new(); queries::save_lineup(&pool, "doomed", &[1], &positions, "standard", None, None) .await .unwrap(); assert!(queries::delete_lineup(&pool, "doomed").await.unwrap()); assert!(!queries::delete_lineup(&pool, "doomed").await.unwrap()); } #[tokio::test] async fn get_lineups_returns_sorted_by_name() { let pool = test_pool().await; let positions = HashMap::new(); queries::save_lineup(&pool, "Zulu", &[1], &positions, "standard", None, None) .await .unwrap(); queries::save_lineup(&pool, "Alpha", &[2], &positions, "standard", None, None) .await .unwrap(); let lineups = queries::get_lineups(&pool).await.unwrap(); assert_eq!(lineups[0].name, "Alpha"); assert_eq!(lineups[1].name, "Zulu"); } // ============================================================================= // Roster Tests // ============================================================================= #[tokio::test] async fn get_my_roster_assembles_majors_il_minors() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; insert_team(&pool, 2, "WVIL", 13).await; insert_team(&pool, 3, "WVMiL", 13).await; insert_player(&pool, 10, "Major Player", 1, 13, "SS").await; insert_player(&pool, 11, "IL Player", 2, 13, "SP").await; insert_player(&pool, 12, "Minor Player", 3, 13, "CF").await; let roster = queries::get_my_roster(&pool, "WV", 13).await.unwrap(); assert_eq!(roster.majors.len(), 1); assert_eq!(roster.il.len(), 1); assert_eq!(roster.minors.len(), 1); assert_eq!(roster.majors[0].name, "Major Player"); assert_eq!(roster.il[0].name, "IL Player"); assert_eq!(roster.minors[0].name, "Minor Player"); } #[tokio::test] async fn get_my_roster_returns_empty_when_team_missing() { let pool = test_pool().await; let roster = queries::get_my_roster(&pool, "NOPE", 13).await.unwrap(); assert!(roster.majors.is_empty()); assert!(roster.il.is_empty()); assert!(roster.minors.is_empty()); } // ============================================================================= // Schema Tests // ============================================================================= #[tokio::test] async fn reset_database_drops_and_recreates() { let pool = test_pool().await; insert_team(&pool, 1, "WV", 13).await; schema::reset_database(&pool).await.unwrap(); let teams = queries::get_all_teams(&pool, 13, false).await.unwrap(); assert!(teams.is_empty(), "tables should be empty after reset"); }