//! Save schema V1 — the pre-versioned shape that shipped on `main`.
//!
//! FROZEN. Do not edit this file except to fix a migration bug. New schema
//! changes go in `v2.rs` together with a `From<GameStateV1> for GameStateV2`
//! conversion.
//!
//! V1 is structurally identical to today's live `GameState`, but kept as a
//! standalone copy so future divergence (V2 introduces per-fingerer
//! modifiers, V3 inevitably something else) doesn't have to read the
//! current code to know what V1 looked like.

use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};

use crate::game::state::GameState;

/// V1 mirror of `Buff`. Frozen at the shape it had when V1 shipped:
/// click-frenzy and per-fingerer-boost variants only. The `FingererBoost`
/// variant is consumed by the V1→V2 chain step (`super::v2`); the V1
/// snapshot itself stays unchanged.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum BuffV1 {
    ClickFrenzy {
        ticks_remaining: u32,
        initial_ticks: u32,
        mult: f64,
    },
    FingererBoost {
        ticks_remaining: u32,
        initial_ticks: u32,
        fingerer_id: String,
        mult: f64,
    },
}

/// V1 game-state snapshot. Holds only the persisted fields — everything
/// `#[serde(skip)]` on the live `GameState` is ephemeral and gets seeded
/// by `migrate_runtime()` after the chain finishes.
#[derive(Clone, Serialize, Deserialize)]
pub struct GameStateV1 {
    #[serde(default)]
    pub cuques: f64,
    #[serde(default)]
    pub total_clicks: u64,
    #[serde(default)]
    pub lifetime_cuques: f64,
    #[serde(default)]
    pub best_fps: f64,
    #[serde(default)]
    pub golden_caught: u64,
    #[serde(default)]
    pub fingerers_owned: HashMap<String, u32>,
    #[serde(default)]
    pub achievements_earned: HashSet<String>,
    #[serde(default)]
    pub upgrades_earned: HashSet<String>,
    #[serde(default)]
    pub prestige: u64,
    #[serde(default)]
    pub total_play_ticks: u64,
    #[serde(default)]
    pub buffs: Vec<BuffV1>,
}

impl GameStateV1 {
    /// Convenience shortcut for tests and tooling: walk this V1 snapshot
    /// through the migration chain to a live `GameState`. Folds through
    /// every intermediate version. Production code goes through
    /// `crate::save::load_from_str` instead, which also runs
    /// `migrate_runtime()`.
    pub fn into_current(self) -> GameState {
        super::v2::GameStateV2::from(self).into_current()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::game::state::Buff;

    #[test]
    fn into_current_preserves_all_persisted_fields() {
        let v1 = GameStateV1 {
            cuques: 12345.6,
            total_clicks: 42,
            lifetime_cuques: 99999.0,
            best_fps: 17.5,
            golden_caught: 3,
            fingerers_owned: [("index_finger".into(), 9), ("latex_glove".into(), 4)]
                .into_iter()
                .collect(),
            achievements_earned: ["first_finger".into()].into_iter().collect(),
            upgrades_earned: ["click_mult_1".into()].into_iter().collect(),
            prestige: 7,
            total_play_ticks: 1000,
            buffs: vec![BuffV1::ClickFrenzy {
                ticks_remaining: 100,
                initial_ticks: 260,
                mult: 777.0,
            }],
        };

        let s = v1.into_current();

        assert_eq!(s.version, crate::save::CURRENT_VERSION);
        assert_eq!(s.cuques, crate::bignum::Mag::from_f64(12345.6));
        assert_eq!(s.total_clicks, 42);
        assert_eq!(s.lifetime_cuques, crate::bignum::Mag::from_f64(99999.0));
        assert_eq!(s.best_fps, crate::bignum::Mag::from_f64(17.5));
        assert_eq!(s.golden_caught, 3);
        assert_eq!(s.fingerer_count("index_finger"), 9);
        assert_eq!(s.fingerer_count("latex_glove"), 4);
        assert!(s.has_achievement("first_finger"));
        // V3→V4 (the 1.0.0 breaking change) silently drops the old
        // hardcoded upgrades. A V1 save's `upgrades_earned` field is
        // accepted by the V1 deserializer but the chain ends with an
        // empty tree.
        assert!(s.tree.bought.is_empty());
        assert_eq!(s.prestige, 7);
        assert_eq!(s.total_play_ticks, 1000);
        assert_eq!(s.buffs.len(), 1);
        assert!(matches!(
            s.buffs[0],
            Buff::ClickFrenzy {
                ticks_remaining: 100,
                ..
            }
        ));
    }

    #[test]
    fn in_flight_fingerer_boost_becomes_purple_modifier() {
        // Active per-fingerer Buff in a V1 save reaches live state as a
        // PurpleCoin modifier on the targeted fingerer with remaining time
        // preserved. The legacy `Buff::FingererBoost` no longer exists.
        let v1 = GameStateV1 {
            buffs: vec![BuffV1::FingererBoost {
                ticks_remaining: 600,
                initial_ticks: 1200,
                fingerer_id: "latex_glove".into(),
                mult: 7.0,
            }],
            ..GameStateV1 {
                cuques: 0.0,
                total_clicks: 0,
                lifetime_cuques: 0.0,
                best_fps: 0.0,
                golden_caught: 0,
                fingerers_owned: HashMap::new(),
                achievements_earned: HashSet::new(),
                upgrades_earned: HashSet::new(),
                prestige: 0,
                total_play_ticks: 0,
                buffs: vec![],
            }
        };

        let s = v1.into_current();

        // Live `Buff` enum is now click-only — FingererBoost is gone.
        assert!(
            s.buffs
                .iter()
                .all(|b| matches!(b, Buff::ClickFrenzy { .. }))
        );

        let st = s
            .fingerers_state
            .get("latex_glove")
            .expect("modifier attached to target fingerer");
        assert_eq!(st.modifiers.len(), 1);
        let m = &st.modifiers[0];
        assert!(matches!(
            m.source,
            crate::game::modifier::ModifierSource::PurpleCoin
        ));
        assert!(matches!(
            m.duration,
            crate::game::modifier::ModifierDuration::Ticks(600)
        ));
        // Remaining time and mult survive.
        assert!((st.aggregate.mul_factor.to_f64() - 7.0).abs() < 1e-9);
    }
}
