use std::collections::HashMap;
use std::fs;

use chrono::{DateTime, TimeZone, Utc};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};

use keydr::config::Config;
use keydr::engine::key_stats::{KeyStatsStore, MASTERY_MIN_SPEED_CONFIDENCE};
use keydr::engine::skill_tree::{
    ALL_BRANCHES, BranchId, BranchProgress, BranchStatus, SkillTreeProgress,
};
use keydr::session::result::{DrillResult, KeyTime};
use keydr::store::schema::{
    DrillHistoryData, EXPORT_VERSION, ExportData, KeyStatsData, ProfileData,
};

const SCHEMA_VERSION: u32 = 4;
const TARGET_CPM: f64 = 175.0;

// ── Helpers ──────────────────────────────────────────────────────────────

/// Per-key target behaviour used to synthesize keystrokes.
///
/// Fixture stats are *derived by replaying* the generated drills rather than
/// hand-written, so persisted `key_stats`/`ranked_key_stats` agree with
/// `drill_history` by construction. That means the intent here is the only place
/// a profile declares "this key should end up mastered": the promotion gate in
/// `key_stats` decides the actual outcome, exactly as it would at runtime.
#[derive(Clone, Copy)]
struct KeyIntent {
    /// Target speed confidence, i.e. target_time_ms / mean correct keystroke time.
    confidence: f64,
    /// Probability that any given keystroke for this key is incorrect.
    error_rate: f64,
    /// What the profile expects the promotion gate to decide. Asserted after replay.
    expect_mastered: bool,
}

impl KeyIntent {
    /// Comfortably clears all three gates. Confidence is kept well above the
    /// speed threshold so per-drill speed variation cannot flip the outcome.
    fn mastered(confidence: f64) -> Self {
        debug_assert!(confidence >= MASTERY_MIN_SPEED_CONFIDENCE + 0.1);
        Self {
            confidence,
            error_rate: 0.02,
            expect_mastered: true,
        }
    }

    /// Still learning: misses the speed gate and the accuracy gate.
    fn in_progress(confidence: f64) -> Self {
        debug_assert!(confidence < MASTERY_MIN_SPEED_CONFIDENCE);
        Self {
            confidence,
            error_rate: 0.12,
            expect_mastered: false,
        }
    }

    /// Misses *only* the speed gate: accuracy and sample count both qualify.
    /// Confidence sits inside [1.0, MASTERY_MIN_SPEED_CONFIDENCE) with enough
    /// margin that per-drill speed variation keeps it in that band, and the key
    /// is typed cleanly so its error-rate EMA is unambiguously under the gate.
    fn near_mastery() -> Self {
        Self {
            confidence: MASTERY_MIN_SPEED_CONFIDENCE - 0.025,
            error_rate: 0.0,
            expect_mastered: false,
        }
    }
}

/// Build an intent map covering every key in `keys`.
fn intents_for(keys: &[char], intent: KeyIntent) -> HashMap<char, KeyIntent> {
    keys.iter().map(|&k| (k, intent)).collect()
}

/// Replay drills into a stats store exactly the way the app does.
/// `ranked_only` selects the authoritative progression store.
fn replay_stats(drills: &[DrillResult], ranked_only: bool) -> KeyStatsStore {
    let mut store = KeyStatsStore {
        target_cpm: TARGET_CPM,
        ..KeyStatsStore::default()
    };
    for drill in drills {
        if ranked_only && !drill.ranked {
            continue;
        }
        for kt in &drill.per_key_times {
            match (kt.correct, ranked_only) {
                // Only ranked updates may promote sticky mastery.
                (true, true) => store.update_key_ranked(kt.key, kt.time_ms),
                (false, true) => store.update_key_error_ranked(kt.key),
                (true, false) => store.update_key(kt.key, kt.time_ms),
                (false, false) => store.update_key_error(kt.key),
            }
        }
    }
    store
}

/// Derive both stores from the generated history and verify the promotion gate
/// produced the mastery outcome each key's intent declared. Panics on drift so a
/// fixture can never silently disagree with the runtime gate.
fn stats_from_drills(
    profile: &str,
    drills: &[DrillResult],
    intents: &HashMap<char, KeyIntent>,
) -> (KeyStatsStore, KeyStatsStore) {
    let key_stats = replay_stats(drills, false);
    let ranked_key_stats = replay_stats(drills, true);

    let mut keys: Vec<char> = intents.keys().copied().collect();
    keys.sort_unstable();
    for key in keys {
        let intent = intents[&key];
        let actual = ranked_key_stats.is_mastered(key);
        assert_eq!(
            actual,
            intent.expect_mastered,
            "{profile}: key {key:?} expected mastered={} but replay produced {actual} \
             (confidence {:.3}, samples {}, error_rate_ema {:.3})",
            intent.expect_mastered,
            ranked_key_stats.get_confidence(key),
            ranked_key_stats
                .get_stat(key)
                .map(|s| s.sample_count)
                .unwrap_or(0),
            ranked_key_stats.smoothed_error_rate(key),
        );
    }
    assert!(
        key_stats.mastered_keys().is_empty(),
        "{profile}: unranked stats must never carry the sticky mastery bit"
    );

    (key_stats, ranked_key_stats)
}

/// Generate monotonic timestamps: base_date + day_offset days + drill_offset * 2min.
fn drill_timestamp(base: DateTime<Utc>, day: u32, drill_in_day: u32) -> DateTime<Utc> {
    base + chrono::Duration::days(day as i64) + chrono::Duration::seconds(drill_in_day as i64 * 120)
}

/// Generate a DrillResult whose keystrokes follow each key's intent.
///
/// `speed_scale` shifts the whole drill slightly so history charts still show
/// natural variation; it is kept small enough that recency-weighted confidence
/// stays on the intended side of the promotion threshold.
fn make_drill_result(
    rng: &mut SmallRng,
    char_count: usize,
    keys: &[char],
    intents: &HashMap<char, KeyIntent>,
    speed_scale: f64,
    timestamp: DateTime<Utc>,
    mode: &str,
    ranked: bool,
) -> DrillResult {
    let target_time_ms = 60000.0 / TARGET_CPM; // ~342.86 ms

    // Emit keys in shuffled passes so every key gets near-equal coverage in each
    // drill (uniform sampling leaves rare keys short of the sample gate) while
    // still varying adjacency for realistic n-gram data.
    let mut order: Vec<char> = Vec::with_capacity(char_count);
    let mut pass: Vec<char> = keys.to_vec();
    while order.len() < char_count {
        for i in (1..pass.len()).rev() {
            pass.swap(i, rng.gen_range(0..=i));
        }
        order.extend(pass.iter().copied());
    }
    order.truncate(char_count);

    let per_key_times: Vec<KeyTime> = order
        .into_iter()
        .enumerate()
        .map(|(i, key)| {
            let intent = intents
                .get(&key)
                .unwrap_or_else(|| panic!("no KeyIntent declared for drill key {key:?}"));
            let is_correct = !rng.gen_bool(intent.error_rate);
            let mean_time = (target_time_ms / intent.confidence) * speed_scale;
            let time_ms = if is_correct {
                mean_time * rng.gen_range(0.96..1.04)
            } else {
                // Errors never feed timing/confidence; keep them plausibly slow.
                mean_time + rng.gen_range(120.0..290.0) + (i as f64 % 5.0) * 8.0
            };
            KeyTime {
                key,
                time_ms: time_ms.max(25.0),
                correct: is_correct,
            }
        })
        .collect();

    // Derive the reported aggregates from the keystrokes actually generated.
    let incorrect = per_key_times.iter().filter(|kt| !kt.correct).count();
    let correct = char_count - incorrect;
    let elapsed_secs = (per_key_times.iter().map(|kt| kt.time_ms).sum::<f64>() / 1000.0).max(1.0);
    let cpm = correct as f64 / elapsed_secs * 60.0;
    let accuracy = if char_count == 0 {
        100.0
    } else {
        (correct as f64 / char_count as f64) * 100.0
    };

    DrillResult {
        wpm: cpm / 5.0,
        cpm,
        accuracy,
        correct,
        incorrect,
        total_chars: char_count,
        elapsed_secs,
        timestamp,
        per_key_times,
        drill_mode: mode.to_string(),
        ranked,
        partial: false,
        completion_percent: 100.0,
    }
}

fn make_skill_tree_progress(branches: Vec<(BranchId, BranchStatus, usize)>) -> SkillTreeProgress {
    let mut map = HashMap::new();
    for (id, status, level) in branches {
        map.insert(
            id.to_key().to_string(),
            BranchProgress {
                status,
                current_level: level,
            },
        );
    }
    // Fill in any missing branches as Locked
    for id in BranchId::all() {
        map.entry(id.to_key().to_string())
            .or_insert(BranchProgress {
                status: BranchStatus::Locked,
                current_level: 0,
            });
    }
    SkillTreeProgress { branches: map }
}

/// Fixed exported_at timestamp for deterministic output.
fn fixed_export_timestamp() -> DateTime<Utc> {
    Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap()
}

/// Canonical config with fixed paths for deterministic output across environments.
fn canonical_config() -> Config {
    Config {
        passage_download_dir: "/tmp/keydr/passages".to_string(),
        code_download_dir: "/tmp/keydr/code".to_string(),
        ..Config::default()
    }
}

fn make_export(
    profile: ProfileData,
    key_stats: KeyStatsStore,
    ranked_key_stats: KeyStatsStore,
    drill_history: Vec<DrillResult>,
) -> ExportData {
    ExportData {
        keydr_export_version: EXPORT_VERSION,
        exported_at: fixed_export_timestamp(),
        config: canonical_config(),
        profile,
        key_stats: KeyStatsData {
            schema_version: SCHEMA_VERSION,
            stats: key_stats,
        },
        ranked_key_stats: KeyStatsData {
            schema_version: SCHEMA_VERSION,
            stats: ranked_key_stats,
        },
        drill_history: DrillHistoryData {
            schema_version: SCHEMA_VERSION,
            drills: drill_history,
        },
    }
}

/// Get all keys for a branch up to (and including) level_index.
fn branch_keys_up_to(branch_id: BranchId, level_index: usize) -> Vec<char> {
    let def = ALL_BRANCHES
        .iter()
        .find(|b| b.id == branch_id)
        .expect("branch not found");
    let mut keys = Vec::new();
    for (i, level) in def.levels.iter().enumerate() {
        if i <= level_index {
            keys.extend_from_slice(level.keys);
        }
    }
    keys
}

/// Get the keys belonging to exactly one level of a branch.
fn branch_level_keys(branch_id: BranchId, level_index: usize) -> Vec<char> {
    let def = ALL_BRANCHES
        .iter()
        .find(|b| b.id == branch_id)
        .expect("branch not found");
    def.levels[level_index].keys.to_vec()
}

/// Get all keys for all levels of a branch.
fn branch_all_keys(branch_id: BranchId) -> Vec<char> {
    let def = ALL_BRANCHES
        .iter()
        .find(|b| b.id == branch_id)
        .expect("branch not found");
    let mut keys = Vec::new();
    for level in def.levels {
        keys.extend_from_slice(level.keys);
    }
    keys
}

/// Lowercase keys: first `count` from frequency order.
fn lowercase_keys(count: usize) -> Vec<char> {
    let def = ALL_BRANCHES
        .iter()
        .find(|b| b.id == BranchId::Lowercase)
        .unwrap();
    def.levels[0].keys[..count].to_vec()
}

/// Base date for all profiles.
fn base_date() -> DateTime<Utc> {
    // Keep generated fixtures in the recent past so activity heatmaps that show
    // only recent weeks still light up when importing test profiles.
    let start_day = Utc::now().date_naive() - chrono::Duration::days(100);
    Utc.from_utc_datetime(&start_day.and_hms_opt(8, 0, 0).unwrap())
}

/// Generate drill history spread across `streak_days` days.
fn generate_drills(
    rng: &mut SmallRng,
    total: usize,
    streak_days: u32,
    keys: &[char],
    intents: &HashMap<char, KeyIntent>,
    mode_distribution: &[(&str, bool, usize)], // (mode, ranked, count)
) -> Vec<DrillResult> {
    let base = base_date();
    let mut drills = Vec::new();
    let mut drill_idx = 0usize;

    for &(mode, ranked, count) in mode_distribution {
        for i in 0..count {
            let day = if streak_days > 0 {
                (drill_idx as u32 * streak_days) / total as u32
            } else {
                0
            };
            let drill_in_day = drill_idx as u32 % 15; // max 15 drills per day spacing
            let ts = drill_timestamp(base, day, drill_in_day);

            // Small per-drill speed variation so history charts are not flat.
            // Confidence is recency-weighted, so this stays tight enough that a
            // key's mastery outcome is decided by its intent, not by which drill
            // happened to land last.
            let speed_scale = 1.0 + ((i % 7) as f64 - 3.0) * 0.006;
            let mut char_count = 80 + (i % 40) + rng.gen_range(0..12);
            // Ranked drills must give every key enough correct samples to reach
            // the sample gate; unranked drills have no such requirement.
            if ranked {
                char_count = char_count.max(keys.len() * 2);
            }

            drills.push(make_drill_result(
                rng,
                char_count,
                keys,
                intents,
                speed_scale,
                ts,
                mode,
                ranked,
            ));
            drill_idx += 1;
        }
    }

    // Sort by timestamp to ensure monotonic ordering
    drills.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
    drills
}

fn last_practice_date_from_drills(drills: &[DrillResult]) -> Option<String> {
    drills
        .last()
        .map(|d| d.timestamp.format("%Y-%m-%d").to_string())
}

fn make_profile_data(
    skill_tree: SkillTreeProgress,
    total_score: f64,
    total_drills: u32,
    streak_days: u32,
    best_streak: u32,
    last_practice_date: Option<String>,
) -> ProfileData {
    let mut skill_tree_by_language = HashMap::new();
    skill_tree_by_language.insert("en".to_string(), skill_tree.clone());
    ProfileData {
        schema_version: SCHEMA_VERSION,
        skill_tree,
        skill_tree_by_language,
        total_score,
        total_drills,
        streak_days,
        best_streak,
        last_practice_date,
    }
}

// ── Profile Builders ─────────────────────────────────────────────────────

fn build_profile_01() -> ExportData {
    let skill_tree =
        make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 0)]);

    make_export(
        make_profile_data(skill_tree, 0.0, 0, 0, 0, None),
        KeyStatsStore::default(),
        KeyStatsStore::default(),
        Vec::new(),
    )
}

fn build_profile_02() -> ExportData {
    // Lowercase InProgress level 4 => 6 + 4 = 10 keys: e,t,a,o,i,n,s,h,r,d
    let skill_tree =
        make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 4)]);

    let all_keys = lowercase_keys(10);
    let mut intents = intents_for(&all_keys[..6], KeyIntent::mastered(1.35)); // e,t,a,o,i,n
    for (i, &k) in all_keys[6..].iter().enumerate() {
        // s,h,r,d — still learning
        intents.insert(k, KeyIntent::in_progress([0.3, 0.5, 0.6, 0.7][i]));
    }

    let mut rng = SmallRng::seed_from_u64(2002);
    let drills = generate_drills(
        &mut rng,
        15,
        3,
        &all_keys,
        &intents,
        &[("adaptive", false, 11), ("adaptive", true, 4)],
    );
    let (key_stats, ranked_stats) = stats_from_drills("02-early-lowercase", &drills, &intents);

    // total_score: level_from_score(x) = (x/100).sqrt() => for level 2: score ~400
    make_export(
        make_profile_data(
            skill_tree,
            350.0,
            15,
            3,
            3,
            last_practice_date_from_drills(&drills),
        ),
        key_stats,
        ranked_stats,
        drills,
    )
}

fn build_profile_03() -> ExportData {
    // Lowercase InProgress level 12 => 6 + 12 = 18 keys through 'y'
    let skill_tree =
        make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 12)]);

    let all_keys = lowercase_keys(18);
    let mut intents = intents_for(&all_keys[..14], KeyIntent::mastered(1.4));
    for (i, &k) in all_keys[14..].iter().enumerate() {
        // w,f,g,y — still learning
        intents.insert(k, KeyIntent::in_progress([0.4, 0.6, 0.7, 0.8][i]));
    }

    let mut rng = SmallRng::seed_from_u64(2003);
    let drills = generate_drills(
        &mut rng,
        50,
        7,
        &all_keys,
        &intents,
        &[("adaptive", false, 35), ("adaptive", true, 15)],
    );
    let (key_stats, ranked_stats) = stats_from_drills("03-mid-lowercase", &drills, &intents);

    // level ~3: score ~900
    make_export(
        make_profile_data(
            skill_tree,
            900.0,
            50,
            7,
            7,
            last_practice_date_from_drills(&drills),
        ),
        key_stats,
        ranked_stats,
        drills,
    )
}

fn build_profile_03_near_lowercase_complete() -> ExportData {
    // Lowercase InProgress level 19 => 6 + 19 = 25 keys unlocked.
    // One unlocked key is just below mastery so a drill can trigger the final unlock popup.
    let skill_tree =
        make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 19)]);

    let all_keys = lowercase_keys(25);
    let near_mastery_key = all_keys[24];

    let mut intents = intents_for(&all_keys[..24], KeyIntent::mastered(1.4));
    // Miss exactly the speed gate while already meeting the sample + accuracy gates.
    intents.insert(near_mastery_key, KeyIntent::near_mastery());

    let mut rng = SmallRng::seed_from_u64(2303);
    let drills = generate_drills(
        &mut rng,
        90,
        10,
        &all_keys,
        &intents,
        &[("adaptive", false, 62), ("adaptive", true, 28)],
    );
    let (key_stats, ranked_stats) =
        stats_from_drills("03-near-lowercase-complete", &drills, &intents);

    make_export(
        make_profile_data(
            skill_tree,
            1800.0,
            90,
            10,
            12,
            last_practice_date_from_drills(&drills),
        ),
        key_stats,
        ranked_stats,
        drills,
    )
}

fn build_profile_04() -> ExportData {
    // Lowercase Complete (level 20), all others Available
    let skill_tree = make_skill_tree_progress(vec![
        (BranchId::Lowercase, BranchStatus::Complete, 20),
        (BranchId::Capitals, BranchStatus::Available, 0),
        (BranchId::Numbers, BranchStatus::Available, 0),
        (BranchId::ProsePunctuation, BranchStatus::Available, 0),
        (BranchId::Whitespace, BranchStatus::Available, 0),
        (BranchId::CodeSymbols, BranchStatus::Available, 0),
    ]);

    let all_keys = lowercase_keys(26);
    // Lowercase is Complete, so every one of its keys must be sticky-mastered.
    let intents = intents_for(&all_keys, KeyIntent::mastered(1.45));

    let mut rng = SmallRng::seed_from_u64(2004);
    let drills = generate_drills(
        &mut rng,
        100,
        14,
        &all_keys,
        &intents,
        &[("adaptive", false, 70), ("adaptive", true, 30)],
    );
    let (key_stats, ranked_stats) = stats_from_drills("04-lowercase-complete", &drills, &intents);

    // level ~5: score ~2500
    make_export(
        make_profile_data(
            skill_tree,
            2500.0,
            100,
            14,
            14,
            last_practice_date_from_drills(&drills),
        ),
        key_stats,
        ranked_stats,
        drills,
    )
}

fn build_profile_05() -> ExportData {
    // Multiple branches in progress
    let skill_tree = make_skill_tree_progress(vec![
        (BranchId::Lowercase, BranchStatus::Complete, 20),
        (BranchId::Capitals, BranchStatus::InProgress, 1),
        (BranchId::Numbers, BranchStatus::InProgress, 0),
        (BranchId::ProsePunctuation, BranchStatus::InProgress, 0),
        (BranchId::Whitespace, BranchStatus::Available, 0),
        (BranchId::CodeSymbols, BranchStatus::Available, 0),
    ]);

    // Build all unlocked keys for drill history
    let mut all_unlocked: Vec<char> = lowercase_keys(26);
    all_unlocked.extend(branch_keys_up_to(BranchId::Capitals, 1));
    all_unlocked.extend(branch_keys_up_to(BranchId::Numbers, 0));
    all_unlocked.extend(branch_keys_up_to(BranchId::ProsePunctuation, 0));

    // Lowercase is Complete and Capitals L1 is behind the current level, so those
    // are mastered. The keys of each branch's *current* level are the ones still
    // being learned — they must not be mastered, or the saved in-progress levels
    // would contradict the authoritative ranked stats.
    let mut intents = intents_for(&lowercase_keys(26), KeyIntent::mastered(1.55));
    intents.extend(intents_for(
        &branch_level_keys(BranchId::Capitals, 0),
        KeyIntent::mastered(1.3),
    ));
    let in_progress_levels = [
        branch_level_keys(BranchId::Capitals, 1),
        branch_level_keys(BranchId::Numbers, 0),
        branch_level_keys(BranchId::ProsePunctuation, 0),
    ];
    for level_keys in &in_progress_levels {
        for (i, &k) in level_keys.iter().enumerate() {
            intents.insert(k, KeyIntent::in_progress(0.35 + (i % 5) as f64 * 0.09));
        }
    }

    let mut rng = SmallRng::seed_from_u64(2005);
    let drills = generate_drills(
        &mut rng,
        200,
        21,
        &all_unlocked,
        &intents,
        &[
            ("adaptive", false, 170),
            ("passage", false, 10),
            ("adaptive", true, 20),
        ],
    );
    let (key_stats, ranked_stats) = stats_from_drills("05-multi-branch", &drills, &intents);

    // level ~7: score ~5000
    make_export(
        make_profile_data(
            skill_tree,
            5000.0,
            200,
            21,
            21,
            last_practice_date_from_drills(&drills),
        ),
        key_stats,
        ranked_stats,
        drills,
    )
}

fn build_profile_06() -> ExportData {
    // Most branches complete, Code Symbols InProgress level 2
    let skill_tree = make_skill_tree_progress(vec![
        (BranchId::Lowercase, BranchStatus::Complete, 20),
        (BranchId::Capitals, BranchStatus::Complete, 3),
        (BranchId::Numbers, BranchStatus::Complete, 2),
        (BranchId::ProsePunctuation, BranchStatus::Complete, 3),
        (BranchId::Whitespace, BranchStatus::Complete, 2),
        (BranchId::CodeSymbols, BranchStatus::InProgress, 2),
    ]);

    // All unlocked keys for drills
    let mut all_unlocked: Vec<char> = lowercase_keys(26);
    all_unlocked.extend(branch_all_keys(BranchId::Capitals));
    all_unlocked.extend(branch_all_keys(BranchId::Numbers));
    all_unlocked.extend(branch_all_keys(BranchId::ProsePunctuation));
    all_unlocked.extend(branch_all_keys(BranchId::Whitespace));
    all_unlocked.extend(branch_keys_up_to(BranchId::CodeSymbols, 2));

    // Everything in a completed branch is mastered; only the Code Symbols
    // current level (L3) is still being learned.
    let mut intents = intents_for(&lowercase_keys(26), KeyIntent::mastered(1.65));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::Capitals),
        KeyIntent::mastered(1.45),
    ));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::Numbers),
        KeyIntent::mastered(1.35),
    ));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::ProsePunctuation),
        KeyIntent::mastered(1.35),
    ));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::Whitespace),
        KeyIntent::mastered(1.25),
    ));
    intents.extend(intents_for(
        &branch_keys_up_to(BranchId::CodeSymbols, 1),
        KeyIntent::mastered(1.25),
    ));
    // Code Symbols L3 is the current level — still in progress. Keys it shares
    // with a completed branch (e.g. '!' from ProsePunctuation L3) stay mastered,
    // so only insert intents for keys not already claimed by a completed branch.
    for (i, &k) in branch_level_keys(BranchId::CodeSymbols, 2)
        .iter()
        .enumerate()
    {
        intents
            .entry(k)
            .or_insert_with(|| KeyIntent::in_progress(0.3 + (i % 4) as f64 * 0.09));
    }

    let mut rng = SmallRng::seed_from_u64(2006);
    let drills = generate_drills(
        &mut rng,
        500,
        45,
        &all_unlocked,
        &intents,
        &[
            ("adaptive", false, 350),
            ("passage", false, 50),
            ("code", false, 50),
            ("adaptive", true, 50),
        ],
    );
    let (key_stats, ranked_stats) = stats_from_drills("06-advanced", &drills, &intents);

    // level ~12: score ~15000
    make_export(
        make_profile_data(
            skill_tree,
            15000.0,
            500,
            45,
            60,
            last_practice_date_from_drills(&drills),
        ),
        key_stats,
        ranked_stats,
        drills,
    )
}

fn build_profile_07() -> ExportData {
    // Everything complete
    let skill_tree = make_skill_tree_progress(vec![
        (BranchId::Lowercase, BranchStatus::Complete, 20),
        (BranchId::Capitals, BranchStatus::Complete, 3),
        (BranchId::Numbers, BranchStatus::Complete, 2),
        (BranchId::ProsePunctuation, BranchStatus::Complete, 3),
        (BranchId::Whitespace, BranchStatus::Complete, 2),
        (BranchId::CodeSymbols, BranchStatus::Complete, 4),
    ]);

    // All keys for drills
    let mut all_keys: Vec<char> = lowercase_keys(26);
    all_keys.extend(branch_all_keys(BranchId::Capitals));
    all_keys.extend(branch_all_keys(BranchId::Numbers));
    all_keys.extend(branch_all_keys(BranchId::ProsePunctuation));
    all_keys.extend(branch_all_keys(BranchId::Whitespace));
    all_keys.extend(branch_all_keys(BranchId::CodeSymbols));

    // Every branch is Complete, so every key must be sticky-mastered.
    let mut intents = intents_for(&lowercase_keys(26), KeyIntent::mastered(1.85));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::Capitals),
        KeyIntent::mastered(1.55),
    ));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::Numbers),
        KeyIntent::mastered(1.45),
    ));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::ProsePunctuation),
        KeyIntent::mastered(1.45),
    ));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::Whitespace),
        KeyIntent::mastered(1.35),
    ));
    intents.extend(intents_for(
        &branch_all_keys(BranchId::CodeSymbols),
        KeyIntent::mastered(1.35),
    ));

    let mut rng = SmallRng::seed_from_u64(2007);
    let drills = generate_drills(
        &mut rng,
        800,
        90,
        &all_keys,
        &intents,
        &[
            ("adaptive", false, 400),
            ("passage", false, 150),
            ("code", false, 150),
            ("adaptive", true, 100),
        ],
    );
    let (key_stats, ranked_stats) = stats_from_drills("07-fully-complete", &drills, &intents);

    // level ~18: score ~35000
    make_export(
        make_profile_data(
            skill_tree,
            35000.0,
            800,
            90,
            90,
            last_practice_date_from_drills(&drills),
        ),
        key_stats,
        ranked_stats,
        drills,
    )
}

// ── Main ─────────────────────────────────────────────────────────────────

fn main() {
    fs::create_dir_all("test-profiles").unwrap();

    let profiles: Vec<(&str, ExportData)> = vec![
        ("01-brand-new", build_profile_01()),
        ("02-early-lowercase", build_profile_02()),
        ("03-mid-lowercase", build_profile_03()),
        (
            "03-near-lowercase-complete",
            build_profile_03_near_lowercase_complete(),
        ),
        ("04-lowercase-complete", build_profile_04()),
        ("05-multi-branch", build_profile_05()),
        ("06-advanced", build_profile_06()),
        ("07-fully-complete", build_profile_07()),
    ];

    for (name, data) in &profiles {
        let json = serde_json::to_string_pretty(data).unwrap();
        let path = format!("test-profiles/{name}.json");
        fs::write(&path, &json).unwrap();
        println!("Wrote {path} ({} bytes)", json.len());
    }

    println!("\nGenerated {} test profiles.", profiles.len());
}
