//! Awards XP on `GameWonEvent`, persists `PlayerProgress`, and emits a
//! `LevelUpEvent` when a win pushes the player to a new level.
//!
//! Configurable storage path:
//! - `ProgressPlugin::default()` uses the platform data dir
//! - `ProgressPlugin::headless()` disables I/O for tests

use std::path::PathBuf;

use bevy::prelude::*;
use solitaire_data::{
    PlayerProgress, load_progress_from, progress_file_path, save_progress_to, xp_for_win,
};

use crate::events::{GameWonEvent, XpAwardedEvent};
use crate::game_plugin::GameMutation;
use crate::resources::GameStateResource;

/// Bevy resource wrapping the current `PlayerProgress`.
#[derive(Resource, Debug, Clone)]
pub struct ProgressResource(pub PlayerProgress);

/// Persistence path for `ProgressResource`. `None` disables I/O.
#[derive(Resource, Debug, Clone)]
pub struct ProgressStoragePath(pub Option<PathBuf>);

/// Fired when a win pushes the player to a new level.
#[derive(Message, Debug, Clone, Copy)]
pub struct LevelUpEvent {
    pub previous_level: u32,
    pub new_level: u32,
    pub total_xp: u64,
}

/// System set for the progress-mutating systems. Downstream plugins that
/// read `ProgressResource` after a win should run `.after(ProgressUpdate)`.
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct ProgressUpdate;

/// Bevy plugin that awards XP on `GameWonEvent`, persists `PlayerProgress`,
/// and emits `LevelUpEvent` whenever a win crosses a level boundary.
///
/// Use `ProgressPlugin::default()` in the main app (reads/writes the platform
/// data directory) and `ProgressPlugin::headless()` in tests (no I/O).
pub struct ProgressPlugin {
    pub storage_path: Option<PathBuf>,
}

impl Default for ProgressPlugin {
    fn default() -> Self {
        Self {
            storage_path: progress_file_path(),
        }
    }
}

impl ProgressPlugin {
    /// Plugin configured with no persistence — for tests and headless apps.
    pub fn headless() -> Self {
        Self { storage_path: None }
    }
}

impl Plugin for ProgressPlugin {
    fn build(&self, app: &mut App) {
        let loaded = match &self.storage_path {
            Some(path) => load_progress_from(path),
            None => PlayerProgress::default(),
        };
        app.insert_resource(ProgressResource(loaded))
            .insert_resource(ProgressStoragePath(self.storage_path.clone()))
            .add_message::<LevelUpEvent>()
            .add_message::<XpAwardedEvent>()
            .add_message::<GameWonEvent>()
            .add_systems(
                Update,
                award_xp_on_win.after(GameMutation).in_set(ProgressUpdate),
            );
    }
}

fn award_xp_on_win(
    mut wins: MessageReader<GameWonEvent>,
    mut levelups: MessageWriter<LevelUpEvent>,
    mut xp_awarded: MessageWriter<XpAwardedEvent>,
    game: Res<GameStateResource>,
    path: Res<ProgressStoragePath>,
    mut progress: ResMut<ProgressResource>,
) {
    for ev in wins.read() {
        let used_undo = game.0.undo_count() > 0;
        let amount = xp_for_win(ev.time_seconds, used_undo);
        let prev_level = progress.0.add_xp(amount);
        xp_awarded.write(XpAwardedEvent { amount });
        if progress.0.leveled_up_from(prev_level) {
            levelups.write(LevelUpEvent {
                previous_level: prev_level,
                new_level: progress.0.level,
                total_xp: progress.0.total_xp,
            });
        }
        if let Some(target) = &path.0
            && let Err(e) = save_progress_to(target, &progress.0)
        {
            warn!("failed to save progress: {e}");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::game_plugin::GamePlugin;
    use crate::table_plugin::TablePlugin;

    fn headless_app() -> App {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins)
            .add_plugins(GamePlugin)
            .add_plugins(TablePlugin)
            .add_plugins(ProgressPlugin::headless());
        app.update();
        app
    }

    #[test]
    fn progress_resource_starts_at_default() {
        let app = headless_app();
        let p = &app.world().resource::<ProgressResource>().0;
        assert_eq!(p, &PlayerProgress::default());
    }

    #[test]
    fn win_awards_base_xp() {
        let mut app = headless_app();
        // Game starts with undo_count = 0, so the no-undo bonus applies.
        app.world_mut().write_message(GameWonEvent {
            score: 500,
            time_seconds: 300, // no speed bonus
        });
        app.update();

        let xp = app.world().resource::<ProgressResource>().0.total_xp;
        // base 50 + no_undo 25 = 75
        assert_eq!(xp, 75);
    }

    #[test]
    fn win_after_undo_grants_no_undo_bonus_off() {
        let mut app = headless_app();
        app.world_mut()
            .resource_mut::<GameStateResource>()
            .0
            .force_test_undos(1);

        app.world_mut().write_message(GameWonEvent {
            score: 500,
            time_seconds: 300,
        });
        app.update();

        let xp = app.world().resource::<ProgressResource>().0.total_xp;
        // base 50 only, since undo was used
        assert_eq!(xp, 50);
    }

    #[test]
    fn fast_win_includes_speed_bonus() {
        let mut app = headless_app();
        app.world_mut().write_message(GameWonEvent {
            score: 500,
            time_seconds: 0,
        });
        app.update();

        // base 50 + speed 50 + no_undo 25 = 125
        let xp = app.world().resource::<ProgressResource>().0.total_xp;
        assert_eq!(xp, 125);
    }

    #[test]
    fn crossing_500_xp_fires_levelup_event() {
        let mut app = headless_app();
        // Pre-load 480 XP so a 75-XP win pushes us over the 500 boundary.
        app.world_mut()
            .resource_mut::<ProgressResource>()
            .0
            .total_xp = 480;

        app.world_mut().write_message(GameWonEvent {
            score: 500,
            time_seconds: 300,
        });
        app.update();

        let events = app.world().resource::<Messages<LevelUpEvent>>();
        let mut cursor = events.get_cursor();
        let fired: Vec<_> = cursor.read(events).copied().collect();
        assert_eq!(fired.len(), 1, "exactly one level-up");
        assert_eq!(fired[0].previous_level, 0);
        assert_eq!(fired[0].new_level, 1);
    }

    #[test]
    fn win_without_level_change_does_not_fire_levelup() {
        let mut app = headless_app();
        app.world_mut().write_message(GameWonEvent {
            score: 500,
            time_seconds: 300,
        });
        app.update();

        let events = app.world().resource::<Messages<LevelUpEvent>>();
        let mut cursor = events.get_cursor();
        assert!(cursor.read(events).next().is_none());
    }

    #[test]
    fn xp_awarded_event_fired_with_correct_amount() {
        let mut app = headless_app();
        // Slow win, no undo → base 50 + no_undo 25 = 75
        app.world_mut().write_message(GameWonEvent {
            score: 500,
            time_seconds: 300,
        });
        app.update();

        let events = app.world().resource::<Messages<XpAwardedEvent>>();
        let mut cursor = events.get_cursor();
        let fired: Vec<_> = cursor.read(events).copied().collect();
        assert_eq!(fired.len(), 1);
        assert_eq!(fired[0].amount, 75);
    }

    #[test]
    fn levelup_event_total_xp_matches_progress_resource() {
        let mut app = headless_app();
        app.world_mut()
            .resource_mut::<ProgressResource>()
            .0
            .total_xp = 480;

        app.world_mut().write_message(GameWonEvent {
            score: 500,
            time_seconds: 300,
        });
        app.update();

        let total_xp = app.world().resource::<ProgressResource>().0.total_xp;
        let events = app.world().resource::<Messages<LevelUpEvent>>();
        let mut cursor = events.get_cursor();
        let fired: Vec<_> = cursor.read(events).copied().collect();
        assert_eq!(fired.len(), 1);
        assert_eq!(fired[0].total_xp, total_xp);
    }

    #[test]
    fn zen_mode_win_awards_base_xp() {
        // Zen mode suppresses score display but XP is still awarded normally.
        // score=0 in the event (Zen keeps score at 0), time=300 (no speed bonus),
        // undo_count=0 so no-undo bonus applies: expected 50+25=75.
        let mut app = headless_app();
        app.world_mut().resource_mut::<GameStateResource>().0.mode =
            solitaire_core::game_state::GameMode::Zen;

        app.world_mut().write_message(GameWonEvent {
            score: 0, // Zen mode keeps score at 0
            time_seconds: 300,
        });
        app.update();

        let xp = app.world().resource::<ProgressResource>().0.total_xp;
        assert_eq!(xp, 75, "Zen win: base 50 + no-undo 25 = 75");
    }
}
