//! Unit tests for effect_convert.rs — spell target resolution and
//! EffectDefinition → Effect conversion via the unified `convert_effect_def`.

use cardgame::cards::{AbilityDefinition, EffectDefinition, TokenDef};
use cardgame::effects::{Effect, EffectTarget, TargetingRule, Trigger};
use cardgame::engine::{convert_effect_def, resolve_spell_target, ConversionContext};
use cardgame::types::{PlayerId, Slot};

// ── resolve_spell_target edge cases ──────────────────────────────────────────
// (basic cases are covered by card_playing_tests.rs)

#[test]
fn test_resolve_target_creature_friendly_slot() {
    // Slots 5-9 target friendly creatures
    let result = resolve_spell_target(
        &TargetingRule::TargetCreature(Default::default()),
        Slot(7),
        PlayerId::PLAYER_ONE,
    );
    let target = result.unwrap();
    match target {
        EffectTarget::Creature { owner, slot } => {
            assert_eq!(owner, PlayerId::PLAYER_ONE);
            assert_eq!(slot, Slot(2)); // 7 - 5 = 2
        }
        _ => panic!("Expected Creature target"),
    }
}

#[test]
fn test_resolve_target_creature_invalid_slot() {
    let result = resolve_spell_target(
        &TargetingRule::TargetCreature(Default::default()),
        Slot(10),
        PlayerId::PLAYER_ONE,
    );
    assert!(result.is_err());
}

#[test]
fn test_resolve_target_enemy_creature_invalid_slot() {
    let result = resolve_spell_target(
        &TargetingRule::TargetEnemyCreature,
        Slot(5),
        PlayerId::PLAYER_ONE,
    );
    assert!(result.is_err());
}

#[test]
fn test_resolve_target_ally_creature_offset_encoding() {
    // Slot(5) is the offset encoding for ally slot 0 (used by legal.rs to avoid action-space collision)
    let result = resolve_spell_target(
        &TargetingRule::TargetAllyCreature,
        Slot(5),
        PlayerId::PLAYER_ONE,
    );
    assert!(result.is_ok());
    let target = result.unwrap();
    assert_eq!(target, EffectTarget::Creature { owner: PlayerId::PLAYER_ONE, slot: Slot(0) });
}

#[test]
fn test_resolve_target_ally_creature_invalid_slot() {
    // Slot(10) is truly out of range for both direct (0-4) and offset (5-9) encodings
    let result = resolve_spell_target(
        &TargetingRule::TargetAllyCreature,
        Slot(10),
        PlayerId::PLAYER_ONE,
    );
    assert!(result.is_err());
}

#[test]
fn test_resolve_target_any_enemy_player() {
    let result = resolve_spell_target(
        &TargetingRule::TargetAny,
        Slot(10),
        PlayerId::PLAYER_ONE,
    );
    let target = result.unwrap();
    assert!(matches!(target, EffectTarget::Player(p) if p == PlayerId::PLAYER_TWO));
}

#[test]
fn test_resolve_target_any_self_player() {
    let result = resolve_spell_target(
        &TargetingRule::TargetAny,
        Slot(11),
        PlayerId::PLAYER_ONE,
    );
    let target = result.unwrap();
    assert!(matches!(target, EffectTarget::Player(p) if p == PlayerId::PLAYER_ONE));
}

#[test]
fn test_resolve_target_slot_valid() {
    let result = resolve_spell_target(
        &TargetingRule::TargetSlot,
        Slot(3),
        PlayerId::PLAYER_ONE,
    );
    let target = result.unwrap();
    match target {
        EffectTarget::Creature { owner, slot } => {
            assert_eq!(owner, PlayerId::PLAYER_ONE);
            assert_eq!(slot, Slot(3));
        }
        _ => panic!("Expected Creature target for TargetSlot"),
    }
}

#[test]
fn test_resolve_target_slot_invalid() {
    let result = resolve_spell_target(
        &TargetingRule::TargetSlot,
        Slot(5),
        PlayerId::PLAYER_ONE,
    );
    assert!(result.is_err());
}

#[test]
fn test_resolve_player_target_enemy() {
    let result = resolve_spell_target(
        &TargetingRule::TargetPlayer,
        Slot(0),
        PlayerId::PLAYER_ONE,
    );
    let target = result.unwrap();
    assert!(matches!(target, EffectTarget::Player(p) if p == PlayerId::PLAYER_TWO));
}

#[test]
fn test_resolve_player_target_self() {
    let result = resolve_spell_target(
        &TargetingRule::TargetPlayer,
        Slot(1),
        PlayerId::PLAYER_ONE,
    );
    let target = result.unwrap();
    assert!(matches!(target, EffectTarget::Player(p) if p == PlayerId::PLAYER_ONE));
}

// ── ExplicitTarget context (replaces effect_def_to_effect_with_target) ───────

fn explicit(def: &EffectDefinition) -> Option<Effect> {
    convert_effect_def(def, ConversionContext::ExplicitTarget {
        target: EffectTarget::Creature { owner: PlayerId::PLAYER_TWO, slot: Slot(0) },
        source_player: PlayerId::PLAYER_ONE,
    })
}

#[test]
fn test_damage_conversion() {
    let def = EffectDefinition::Damage { amount: 3, filter: None };
    match explicit(&def).unwrap() {
        Effect::Damage { amount, filter, .. } => {
            assert_eq!(amount, 3);
            assert!(filter.is_none());
        }
        _ => panic!("Expected Damage effect"),
    }
}

#[test]
fn test_heal_conversion() {
    let def = EffectDefinition::Heal { amount: 2, filter: None };
    match explicit(&def).unwrap() {
        Effect::Heal { amount, filter, .. } => {
            assert_eq!(amount, 2);
            assert!(filter.is_none());
        }
        _ => panic!("Expected Heal effect"),
    }
}

#[test]
fn test_draw_always_targets_caster() {
    let def = EffectDefinition::Draw { count: 2 };
    match explicit(&def).unwrap() {
        Effect::Draw { player, count } => {
            assert_eq!(player, PlayerId::PLAYER_ONE);
            assert_eq!(count, 2);
        }
        _ => panic!("Expected Draw effect"),
    }
}

#[test]
fn test_buff_stats_conversion() {
    let def = EffectDefinition::BuffStats { attack: 2, health: 3, filter: None };
    match explicit(&def).unwrap() {
        Effect::BuffStats { attack, health, .. } => {
            assert_eq!(attack, 2);
            assert_eq!(health, 3);
        }
        _ => panic!("Expected BuffStats effect"),
    }
}

#[test]
fn test_destroy_conversion() {
    let def = EffectDefinition::Destroy { filter: None };
    assert!(explicit(&def).is_some());
}

#[test]
fn test_gain_essence_targets_caster() {
    let def = EffectDefinition::GainEssence { amount: 2 };
    match explicit(&def).unwrap() {
        Effect::GainEssence { player, amount } => {
            assert_eq!(player, PlayerId::PLAYER_ONE);
            assert_eq!(amount, 2);
        }
        _ => panic!("Expected GainEssence effect"),
    }
}

#[test]
fn test_refresh_creature_conversion() {
    let def = EffectDefinition::RefreshCreature;
    assert!(matches!(explicit(&def).unwrap(), Effect::RefreshCreature { .. }));
}

#[test]
fn test_copy_conversion() {
    let def = EffectDefinition::Copy;
    match explicit(&def).unwrap() {
        Effect::Copy { owner, .. } => {
            assert_eq!(owner, PlayerId::PLAYER_ONE);
        }
        _ => panic!("Expected Copy effect"),
    }
}

// ── CombatTrigger context (replaces effect_def_to_triggered_effect) ───────────

fn make_ability(targeting: TargetingRule) -> AbilityDefinition {
    AbilityDefinition {
        trigger: Trigger::OnPlay,
        targeting,
        essence_cost: 0,
        effects: vec![],
        conditional_effects: vec![],
    }
}

fn combat(def: &EffectDefinition, targeting: TargetingRule) -> Option<Effect> {
    let ability = make_ability(targeting);
    convert_effect_def(def, ConversionContext::CombatTrigger {
        source_owner: PlayerId::PLAYER_ONE,
        source_slot: Slot(0),
        ability: &ability,
    })
}

#[test]
fn test_triggered_damage_defaults_to_all_enemies() {
    let def = EffectDefinition::Damage { amount: 1, filter: None };
    match combat(&def, TargetingRule::NoTarget).unwrap() {
        Effect::Damage { target, .. } => {
            assert!(
                matches!(target, EffectTarget::AllEnemyCreatures(_)),
                "NoTarget triggered damage should hit all enemy creatures"
            );
        }
        _ => panic!("Expected Damage effect"),
    }
}

#[test]
fn test_triggered_damage_enemy_player_targeting() {
    let def = EffectDefinition::Damage { amount: 2, filter: None };
    match combat(&def, TargetingRule::TargetEnemyPlayer).unwrap() {
        Effect::Damage { target, .. } => {
            assert!(matches!(target, EffectTarget::Player(p) if p == PlayerId::PLAYER_TWO));
        }
        _ => panic!("Expected Damage effect"),
    }
}

#[test]
fn test_triggered_heal_targets_self() {
    let def = EffectDefinition::Heal { amount: 2, filter: None };
    match combat(&def, TargetingRule::NoTarget).unwrap() {
        Effect::Heal { target, .. } => {
            assert!(matches!(
                target,
                EffectTarget::Creature { owner: PlayerId::PLAYER_ONE, slot: Slot(0) }
            ));
        }
        _ => panic!("Expected Heal effect"),
    }
}

#[test]
fn test_triggered_buff_targets_self() {
    let def = EffectDefinition::BuffStats { attack: 1, health: 1, filter: None };
    match combat(&def, TargetingRule::NoTarget).unwrap() {
        Effect::BuffStats { target, .. } => {
            assert!(matches!(target, EffectTarget::Creature { slot: Slot(0), .. }));
        }
        _ => panic!("Expected BuffStats effect"),
    }
}

#[test]
fn test_triggered_draw_targets_owner() {
    let def = EffectDefinition::Draw { count: 1 };
    let ability = make_ability(TargetingRule::NoTarget);
    let effect = convert_effect_def(&def, ConversionContext::CombatTrigger {
        source_owner: PlayerId::PLAYER_TWO,
        source_slot: Slot(0),
        ability: &ability,
    });
    match effect.unwrap() {
        Effect::Draw { player, count } => {
            assert_eq!(player, PlayerId::PLAYER_TWO);
            assert_eq!(count, 1);
        }
        _ => panic!("Expected Draw effect"),
    }
}

#[test]
fn test_triggered_destroy_returns_none() {
    let def = EffectDefinition::Destroy { filter: None };
    assert!(combat(&def, TargetingRule::NoTarget).is_none(), "Destroy should return None for triggered abilities");
}

#[test]
fn test_triggered_transform_returns_none() {
    let def = EffectDefinition::Transform {
        into: TokenDef { name: "Token".to_string(), attack: 1, health: 1, keywords: vec![], abilities: vec![] },
    };
    assert!(combat(&def, TargetingRule::NoTarget).is_none(), "Transform should return None for triggered abilities");
}

#[test]
fn test_triggered_copy_returns_none() {
    let def = EffectDefinition::Copy;
    assert!(combat(&def, TargetingRule::NoTarget).is_none(), "Copy should return None for triggered abilities");
}

#[test]
fn test_triggered_gain_essence() {
    let def = EffectDefinition::GainEssence { amount: 1 };
    match combat(&def, TargetingRule::NoTarget).unwrap() {
        Effect::GainEssence { player, amount } => {
            assert_eq!(player, PlayerId::PLAYER_ONE);
            assert_eq!(amount, 1);
        }
        _ => panic!("Expected GainEssence effect"),
    }
}

#[test]
fn test_triggered_bounce_defaults_to_enemy_creatures() {
    let def = EffectDefinition::Bounce { filter: None };
    match combat(&def, TargetingRule::NoTarget).unwrap() {
        Effect::Bounce { target, .. } => {
            assert!(matches!(target, EffectTarget::AllEnemyCreatures(_)));
        }
        _ => panic!("Expected Bounce effect"),
    }
}

// ── Compile-time exhaustiveness guard ────────────────────────────────────────
//
// This test exists ONLY to give the compiler a single place that enumerates
// every `EffectDefinition` variant. If a new variant is added without updating
// `convert_effect_def`, the non-exhaustive `match` inside the private helpers
// causes a **compile error here**, not a silent `None` at runtime.
//
// The test does not assert return values — passing is sufficient.
#[test]
fn all_effect_def_variants_handled_in_every_context() {
    let owner = PlayerId::PLAYER_ONE;
    let slot = Slot(0);
    let ability = make_ability(TargetingRule::NoTarget);
    let token = TokenDef { name: "T".to_string(), attack: 1, health: 1, keywords: vec![], abilities: vec![] };

    let variants: Vec<EffectDefinition> = vec![
        EffectDefinition::Damage { amount: 1, filter: None },
        EffectDefinition::Heal { amount: 1, filter: None },
        EffectDefinition::Draw { count: 1 },
        EffectDefinition::BuffStats { attack: 1, health: 1, filter: None },
        EffectDefinition::BuffStatsTemporal { attack: 1, health: 0 },
        EffectDefinition::Destroy { filter: None },
        EffectDefinition::GrantKeyword { keyword: "Rush".to_string(), filter: None },
        EffectDefinition::RemoveKeyword { keyword: "Rush".to_string(), filter: None },
        EffectDefinition::Silence { filter: None },
        EffectDefinition::GainEssence { amount: 1 },
        EffectDefinition::RefreshCreature,
        EffectDefinition::Bounce { filter: None },
        EffectDefinition::SummonToken { token: token.clone() },
        EffectDefinition::Transform { into: token.clone() },
        EffectDefinition::Copy,
    ];

    for def in &variants {
        let _ = convert_effect_def(def, ConversionContext::CreatureTrigger { source_owner: owner, source_slot: slot });
        let _ = convert_effect_def(def, ConversionContext::CombatTrigger { source_owner: owner, source_slot: slot, ability: &ability });
        let _ = convert_effect_def(def, ConversionContext::ExplicitTarget {
            target: EffectTarget::Creature { owner, slot },
            source_player: owner,
        });
        let _ = convert_effect_def(def, ConversionContext::SupportTrigger { source_owner: owner, ability: &ability });
        let _ = convert_effect_def(def, ConversionContext::CommanderTrigger { source_owner: owner });
        let _ = convert_effect_def(def, ConversionContext::PrepareTrigger {
            source_player: owner,
            source_slot: Some(slot),
            resolved_target: Some(EffectTarget::Creature { owner, slot }),
        });
        let _ = convert_effect_def(def, ConversionContext::AmbushTrigger {
            source_player: owner,
            attacker_player: owner.opponent(),
            attacker_slot: slot,
        });
    }
}
