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");
}
