//! End-to-end determinism + public-API smoke tests (spec §10).
//!
//! The whole analysis story rests on one guarantee: a fight is a bit-reproducible
//! function of `(rules, seed, actors, controllers)`. These tests drive the crate
//! exactly as `combat-sim` does and assert that guarantee at the event-log level.

use combat_core::actor::{compile_actor, StatBlock};
use combat_core::config::CombatConfig;
use combat_core::controller::{Controller, HoldAndPunish, ScriptedPlayer};
use combat_core::engine::{Encounter, EncounterOptions};
use combat_core::{Actor, ActorId};
use std::collections::BTreeMap;

fn duelist(name: &str, team: u8, level: i32) -> StatBlock {
    let mut competency = BTreeMap::new();
    competency.insert("martial".to_string(), 50);
    competency.insert("restoration".to_string(), 30);
    StatBlock {
        name: name.into(),
        team,
        level,
        max_vigor: 120,
        regen_idle_per_sec: 12,
        regen_combat_per_sec: 6,
        armor_bp: 1500,
        competency,
        loadout: vec![
            "auto_attack".into(),
            "quick_jab".into(),
            "heavy_strike".into(),
            "second_wind".into(),
            "guard".into(),
        ],
        start_vigor_bp: 10_000,
        start_fatigue_bp: 0,
    }
}

fn config() -> CombatConfig {
    let json = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../configs/default.json"))
        .expect("default config readable");
    serde_json::from_str(&json).expect("default config parses")
}

fn build(cfg: &CombatConfig, blocks: &[StatBlock]) -> Vec<Actor> {
    let rules = cfg.compile();
    blocks
        .iter()
        .enumerate()
        .map(|(i, sb)| {
            let (loadout, missing) = rules.resolve_loadout(&sb.loadout);
            assert!(missing.is_empty(), "config loadout has unknown abilities: {missing:?}");
            compile_actor(i as ActorId, sb, loadout, rules.tick_rate)
        })
        .collect()
}

fn controllers() -> Vec<Box<dyn Controller>> {
    vec![Box::new(ScriptedPlayer::new(80)), Box::new(HoldAndPunish::default())]
}

#[test]
fn full_fight_event_log_is_bit_reproducible() {
    let cfg = config();
    let blocks = vec![duelist("P", 0, 10), duelist("E", 1, 12)];
    let opts = EncounterOptions { max_ticks: 6_000, log_events: true, sample_every: 0 };

    let run = || {
        let actors = build(&cfg, &blocks);
        Encounter::new(actors, controllers(), cfg.compile(), 12345, opts).run_with_events()
    };
    let (m1, e1) = run();
    let (m2, e2) = run();

    assert_eq!(m1.ticks, m2.ticks);
    assert_eq!(format!("{:?}", m1.outcome), format!("{:?}", m2.outcome));
    // Event logs must be identical event-for-event — the strongest determinism
    // claim the crate makes.
    let j1 = serde_json::to_string(&e1).unwrap();
    let j2 = serde_json::to_string(&e2).unwrap();
    assert_eq!(j1, j2, "event logs diverged under identical inputs");
    assert!(!e1.is_empty(), "a real fight should emit events");
}

#[test]
fn different_seeds_can_diverge() {
    let cfg = config();
    let blocks = vec![duelist("P", 0, 10), duelist("E", 1, 10)];
    let opts = EncounterOptions::default();

    let outcome_for = |seed: u64| {
        let actors = build(&cfg, &blocks);
        let m = Encounter::new(actors, controllers(), cfg.compile(), seed, opts).run();
        (m.ticks, format!("{:?}", m.outcome))
    };
    // Across many seeds an even-con fight produces a spread of durations (not a
    // single fixed value), evidence the RNG actually drives variety.
    let durations: std::collections::BTreeSet<u64> =
        (0..40).map(|s| outcome_for(s).0).collect();
    assert!(durations.len() > 3, "expected varied fight lengths across seeds");
}

#[test]
fn config_round_trips_and_compiles() {
    let cfg = config();
    let rules = cfg.compile();
    // The shipped library resolves and the headline abilities exist.
    for id in ["auto_attack", "quick_jab", "heavy_strike", "second_wind", "guard", "fireball"] {
        assert!(rules.abilities.contains_key(id), "missing ability {id}");
    }
    // Re-serialize and re-parse to confirm the config is stable through serde.
    let j = serde_json::to_string(&cfg).unwrap();
    let back: CombatConfig = serde_json::from_str(&j).unwrap();
    assert_eq!(back.compile().abilities.len(), rules.abilities.len());
}
