//! Unit tests for Zobrist hashing and transposition table.

use cardgame::bots::transposition::{zobrist_hash, Bound, TranspositionTable};
use cardgame::state::GameState;
use cardgame::types::PlayerId;

fn create_base_state() -> GameState {
    let mut state = GameState::new();
    state.players[0].life = 20;
    state.players[1].life = 20;
    state.current_turn = 1;
    state.active_player = PlayerId::PLAYER_ONE;
    state
}

// --- Zobrist Hash Tests ---

#[test]
fn test_zobrist_hash_deterministic() {
    let state = create_base_state();
    let hash1 = zobrist_hash(&state);
    let hash2 = zobrist_hash(&state);
    assert_eq!(hash1, hash2, "Same state should produce same hash");
}

#[test]
fn test_zobrist_hash_nonzero() {
    let state = create_base_state();
    let hash = zobrist_hash(&state);
    // Hash of any meaningful state should be non-zero (extremely unlikely to be zero)
    assert_ne!(hash, 0, "Hash should be non-zero for a real state");
}

#[test]
fn test_zobrist_hash_different_life() {
    let mut state1 = create_base_state();
    let mut state2 = create_base_state();

    state1.players[0].life = 20;
    state2.players[0].life = 15;

    let hash1 = zobrist_hash(&state1);
    let hash2 = zobrist_hash(&state2);
    assert_ne!(hash1, hash2, "Different life totals should produce different hashes");
}

#[test]
fn test_zobrist_hash_different_active_player() {
    let mut state1 = create_base_state();
    let mut state2 = create_base_state();

    state1.active_player = PlayerId::PLAYER_ONE;
    state2.active_player = PlayerId::PLAYER_TWO;

    let hash1 = zobrist_hash(&state1);
    let hash2 = zobrist_hash(&state2);
    assert_ne!(
        hash1, hash2,
        "Different active player should produce different hashes"
    );
}

#[test]
fn test_zobrist_hash_different_turn() {
    let mut state1 = create_base_state();
    let mut state2 = create_base_state();

    state1.current_turn = 1;
    state2.current_turn = 5;

    let hash1 = zobrist_hash(&state1);
    let hash2 = zobrist_hash(&state2);
    assert_ne!(hash1, hash2, "Different turns should produce different hashes");
}

#[test]
fn test_zobrist_hash_different_essence() {
    let mut state1 = create_base_state();
    let mut state2 = create_base_state();

    state1.players[0].current_essence = 3;
    state2.players[0].current_essence = 5;

    let hash1 = zobrist_hash(&state1);
    let hash2 = zobrist_hash(&state2);
    assert_ne!(
        hash1, hash2,
        "Different essence should produce different hashes"
    );
}

// --- TranspositionTable Tests ---

#[test]
fn test_tt_creation() {
    let tt = TranspositionTable::new(1024);
    assert_eq!(tt.hits, 0);
    assert_eq!(tt.misses, 0);
    assert_eq!(tt.collisions, 0);
}

#[test]
fn test_tt_creation_memory_mb() {
    let tt = TranspositionTable::with_memory_mb(1);
    // Should have entries (exact count depends on entry size)
    assert_eq!(tt.hits, 0);
}

#[test]
fn test_tt_store_and_probe() {
    let mut tt = TranspositionTable::new(1024);

    let hash: u64 = 0xDEAD_BEEF;
    tt.store(hash, 4, 1.5, Bound::Exact, None);

    let entry = tt.probe(hash);
    assert!(entry.is_some(), "Should find stored entry");

    let entry = entry.unwrap();
    assert_eq!(entry.hash, hash);
    assert_eq!(entry.depth, 4);
    assert!((entry.score - 1.5).abs() < f32::EPSILON);
    assert_eq!(entry.bound, Bound::Exact);
}

#[test]
fn test_tt_probe_miss() {
    let mut tt = TranspositionTable::new(1024);
    let result = tt.probe(12345);
    assert!(result.is_none());
    assert_eq!(tt.misses, 1);
}

#[test]
fn test_tt_replace_greater_depth() {
    let mut tt = TranspositionTable::new(1024);

    let hash: u64 = 0xCAFE_BABE;
    tt.store(hash, 2, 1.0, Bound::Lower, None);
    tt.store(hash, 4, 2.0, Bound::Exact, None);

    let entry = tt.probe(hash).unwrap();
    assert_eq!(entry.depth, 4, "Should be replaced by deeper entry");
    assert!((entry.score - 2.0).abs() < f32::EPSILON);
}

#[test]
fn test_tt_clear() {
    let mut tt = TranspositionTable::new(1024);

    tt.store(0xDEAD, 4, 1.0, Bound::Exact, None);
    tt.probe(0xDEAD); // hit
    tt.probe(0xBEEF); // miss

    tt.clear();

    assert_eq!(tt.hits, 0);
    assert_eq!(tt.misses, 0);
    assert_eq!(tt.collisions, 0);
    assert!(tt.probe(0xDEAD).is_none(), "Should be empty after clear");
}

#[test]
fn test_tt_new_search_generation() {
    let mut tt = TranspositionTable::new(1024);

    let hash: u64 = 0x1234;
    tt.store(hash, 2, 1.0, Bound::Lower, None);

    // Advance generation
    tt.new_search();

    // Old entry should be replaceable by shallower search in new generation
    tt.store(hash, 1, 0.5, Bound::Exact, None);

    let entry = tt.probe(hash).unwrap();
    // New generation entry should have replaced old one
    assert_eq!(entry.depth, 1);
}

#[test]
fn test_tt_bound_types() {
    let mut tt = TranspositionTable::new(1024);

    tt.store(1, 4, 1.0, Bound::Exact, None);
    tt.store(2, 4, 2.0, Bound::Lower, None);
    tt.store(3, 4, 3.0, Bound::Upper, None);

    assert_eq!(tt.probe(1).unwrap().bound, Bound::Exact);
    assert_eq!(tt.probe(2).unwrap().bound, Bound::Lower);
    assert_eq!(tt.probe(3).unwrap().bound, Bound::Upper);
}

#[test]
fn test_tt_hit_miss_counting() {
    let mut tt = TranspositionTable::new(1024);

    tt.store(42, 4, 1.0, Bound::Exact, None);

    tt.probe(42); // hit
    tt.probe(42); // hit
    tt.probe(99); // miss

    assert_eq!(tt.hits, 2);
    assert_eq!(tt.misses, 1);
}

#[test]
fn test_tt_non_power_of_two_size() {
    // Size hint that isn't power of 2 should round up without panic
    let mut tt = TranspositionTable::new(1000);
    // Should work fine — store and retrieve
    tt.store(42, 1, 1.0, Bound::Exact, None);
    assert!(tt.probe(42).is_some());
}
