/// Integration tests for the full calc pipeline:
/// league stats → score cache → calculate_matchup (real-time vs cached).
///
/// These tests verify that:
/// - `rebuild_score_cache` populates the DB correctly
/// - `calculate_matchup` (real-time) and `calculate_matchup_cached` agree
/// - Switch-hitter resolution works end-to-end
/// - Batter-only rating (no pitcher card) works end-to-end
use sba_scout::{
    calc::{
        league_stats::BatterLeagueStats,
        matchup::{calculate_matchup, calculate_matchup_cached},
        score_cache::rebuild_score_cache,
    },
    db::{models::Player, queries, schema},
};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

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
}

async fn insert_team(pool: &sqlx::SqlitePool, id: i64, abbrev: &str) {
    sqlx::query(
        "INSERT INTO teams (id, abbrev, short_name, long_name, season) VALUES (?, ?, ?, ?, 13)",
    )
    .bind(id)
    .bind(abbrev)
    .bind(abbrev)
    .bind(format!("{abbrev} Long Name"))
    .execute(pool)
    .await
    .expect("failed to insert team");
}

async fn insert_player(
    pool: &sqlx::SqlitePool,
    id: i64,
    name: &str,
    team_id: i64,
    pos_1: &str,
    hand: &str,
) {
    sqlx::query(
        "INSERT INTO players (id, name, season, team_id, pos_1, hand) VALUES (?, ?, 13, ?, ?, ?)",
    )
    .bind(id)
    .bind(name)
    .bind(team_id)
    .bind(pos_1)
    .bind(hand)
    .execute(pool)
    .await
    .expect("failed to insert player");
}

/// Fetch a Player struct from the DB by id.
async fn fetch_player(pool: &sqlx::SqlitePool, id: i64) -> Player {
    sqlx::query_as("SELECT * FROM players WHERE id = ?")
        .bind(id)
        .fetch_one(pool)
        .await
        .expect("player not found")
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

/// Verify rebuild_score_cache runs without error and populates cache entries.
///
/// With one batter card and one pitcher card, we expect 4 cache rows total:
/// 2 splits for the batter (vlhp, vrhp) and 2 for the pitcher (vlhb, vrhb).
#[tokio::test]
async fn rebuild_score_cache_populates_entries() {
    let pool = test_pool().await;
    insert_team(&pool, 1, "TST").await;
    insert_player(&pool, 10, "Test Batter", 1, "CF", "R").await;
    insert_player(&pool, 11, "Test Pitcher", 1, "SP", "R").await;

    sqlx::query("INSERT INTO batter_cards (player_id) VALUES (?)")
        .bind(10)
        .execute(&pool)
        .await
        .unwrap();

    sqlx::query("INSERT INTO pitcher_cards (player_id) VALUES (?)")
        .bind(11)
        .execute(&pool)
        .await
        .unwrap();

    let result = rebuild_score_cache(&pool).await.expect("rebuild failed");

    assert_eq!(result.batter_splits, 2, "expected 2 batter cache rows (vlhp, vrhp)");
    assert_eq!(result.pitcher_splits, 2, "expected 2 pitcher cache rows (vlhb, vrhb)");

    // Verify rows exist in DB
    let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM standardized_score_cache")
        .fetch_one(&pool)
        .await
        .unwrap();
    assert_eq!(count, 4);
}

/// Verify that calculate_matchup (real-time) and calculate_matchup_cached agree
/// on rating and tier for a standard R batter vs R pitcher matchup.
///
/// All card stats default to 0.0 (zeroed). With zeroed cards and default league
/// stats, both functions must return identical ratings.
#[tokio::test]
async fn realtime_and_cached_matchup_agree() {
    let pool = test_pool().await;
    insert_team(&pool, 1, "TST").await;
    insert_player(&pool, 10, "Test Batter", 1, "CF", "R").await;
    insert_player(&pool, 11, "Test Pitcher", 1, "SP", "R").await;

    sqlx::query("INSERT INTO batter_cards (player_id) VALUES (?)")
        .bind(10)
        .execute(&pool)
        .await
        .unwrap();
    sqlx::query("INSERT INTO pitcher_cards (player_id) VALUES (?)")
        .bind(11)
        .execute(&pool)
        .await
        .unwrap();

    rebuild_score_cache(&pool).await.expect("rebuild failed");

    let batter = fetch_player(&pool, 10).await;
    let pitcher = fetch_player(&pool, 11).await;
    let batter_card = queries::get_batter_card(&pool, 10).await.unwrap().unwrap();
    let pitcher_card = queries::get_pitcher_card(&pool, 11).await.unwrap().unwrap();

    let b_stats = BatterLeagueStats::default();
    let p_stats = sba_scout::calc::league_stats::PitcherLeagueStats::default();

    let realtime = calculate_matchup(
        &batter,
        Some(&batter_card),
        &pitcher,
        Some(&pitcher_card),
        &b_stats,
        &p_stats,
    );

    let cached = calculate_matchup_cached(&pool, &batter, Some(&batter_card), &pitcher, Some(&pitcher_card))
        .await
        .expect("cached matchup failed");

    let rt_rating = realtime.rating.expect("real-time should have rating");
    let c_rating = cached.rating.expect("cached should have rating");

    assert!(
        (rt_rating - c_rating).abs() < 0.01,
        "ratings differ: real-time={rt_rating}, cached={c_rating}"
    );
    assert_eq!(realtime.tier, cached.tier, "tiers should match");
    assert_eq!(realtime.batter_split, cached.batter_split);
    assert_eq!(realtime.pitcher_split, cached.pitcher_split);
}

/// Verify switch-hitter resolution: a player with hand="S" facing an R pitcher
/// should bat left (effective_hand="L", batter_split="vRHP", pitcher_split="vLHB").
#[tokio::test]
async fn switch_hitter_resolution_vs_right_pitcher() {
    let pool = test_pool().await;
    insert_team(&pool, 1, "TST").await;
    insert_player(&pool, 10, "Switch Batter", 1, "CF", "S").await;
    insert_player(&pool, 11, "Right Pitcher", 1, "SP", "R").await;

    sqlx::query("INSERT INTO batter_cards (player_id) VALUES (?)")
        .bind(10)
        .execute(&pool)
        .await
        .unwrap();

    rebuild_score_cache(&pool).await.expect("rebuild failed");

    let batter = fetch_player(&pool, 10).await;
    let pitcher = fetch_player(&pool, 11).await;
    let batter_card = queries::get_batter_card(&pool, 10).await.unwrap().unwrap();

    let b_stats = BatterLeagueStats::default();
    let p_stats = sba_scout::calc::league_stats::PitcherLeagueStats::default();

    let result = calculate_matchup(&batter, Some(&batter_card), &pitcher, None, &b_stats, &p_stats);

    assert_eq!(result.batter_hand, "L", "switch hitter vs R pitcher should bat left");
    assert_eq!(result.batter_split, "vRHP");
    assert_eq!(result.pitcher_split, "vLHB");
    assert!(result.rating.is_some());
}

/// Verify switch-hitter resolution: hand="S" facing an L pitcher → bats right.
#[tokio::test]
async fn switch_hitter_resolution_vs_left_pitcher() {
    let pool = test_pool().await;
    insert_team(&pool, 1, "TST").await;
    insert_player(&pool, 10, "Switch Batter", 1, "CF", "S").await;
    insert_player(&pool, 11, "Left Pitcher", 1, "SP", "L").await;

    sqlx::query("INSERT INTO batter_cards (player_id) VALUES (?)")
        .bind(10)
        .execute(&pool)
        .await
        .unwrap();

    rebuild_score_cache(&pool).await.expect("rebuild failed");

    let batter = fetch_player(&pool, 10).await;
    let pitcher = fetch_player(&pool, 11).await;
    let batter_card = queries::get_batter_card(&pool, 10).await.unwrap().unwrap();

    let b_stats = BatterLeagueStats::default();
    let p_stats = sba_scout::calc::league_stats::PitcherLeagueStats::default();

    let result = calculate_matchup(&batter, Some(&batter_card), &pitcher, None, &b_stats, &p_stats);

    assert_eq!(result.batter_hand, "R", "switch hitter vs L pitcher should bat right");
    assert_eq!(result.batter_split, "vLHP");
    assert_eq!(result.pitcher_split, "vRHB");
}

/// Verify batter-only rating: when pitcher card is None, total equals batter component.
///
/// With a zeroed batter card and default league stats, all stats standardize to 3
/// and the batter component = 3 * 22 = 66.
#[tokio::test]
async fn batter_only_rating_when_no_pitcher_card() {
    let pool = test_pool().await;
    insert_team(&pool, 1, "TST").await;
    insert_player(&pool, 10, "Test Batter", 1, "CF", "R").await;
    insert_player(&pool, 11, "Test Pitcher", 1, "SP", "R").await;

    sqlx::query("INSERT INTO batter_cards (player_id) VALUES (?)")
        .bind(10)
        .execute(&pool)
        .await
        .unwrap();

    rebuild_score_cache(&pool).await.expect("rebuild failed");

    let batter = fetch_player(&pool, 10).await;
    let pitcher = fetch_player(&pool, 11).await;
    let batter_card = queries::get_batter_card(&pool, 10).await.unwrap().unwrap();

    let b_stats = BatterLeagueStats::default();
    let p_stats = sba_scout::calc::league_stats::PitcherLeagueStats::default();

    // Real-time: no pitcher card passed
    let rt = calculate_matchup(&batter, Some(&batter_card), &pitcher, None, &b_stats, &p_stats);
    let rating = rt.rating.expect("should have rating");
    assert!(
        (rating - 66.0).abs() < f64::EPSILON,
        "batter-only zeroed rating should be 66.0, got {rating}"
    );
    assert!(rt.pitcher_component.is_none());

    // Cached: no pitcher card passed (pitcher has no card in DB)
    let cached = calculate_matchup_cached(&pool, &batter, Some(&batter_card), &pitcher, None)
        .await
        .expect("cached matchup failed");
    let c_rating = cached.rating.expect("cached should have rating");
    assert!(
        (c_rating - 66.0).abs() < f64::EPSILON,
        "cached batter-only zeroed rating should be 66.0, got {c_rating}"
    );
}
