//! Weekly goal definitions and helpers.
//!
//! Goals reset every ISO week. Engine evaluates them on `GameWonEvent` and
//! increments matching counters in `PlayerProgress::weekly_goal_progress`.

use chrono::{Datelike, NaiveDate};
use solitaire_core::DrawStockConfig;

/// XP awarded each time a weekly goal is just completed.
pub const WEEKLY_GOAL_XP: u64 = 75;

/// Discriminant for the type of weekly goal the player is working toward.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WeeklyGoalKind {
    /// Any win counts.
    WinGame,
    /// A win without using `undo` counts.
    WinWithoutUndo,
    /// A win in strictly fewer than `seconds` seconds counts.
    WinUnder { seconds: u64 },
    /// A win in Draw-3 mode counts.
    WinDrawThree,
}

/// Static definition of a weekly goal — the goal type, target value, and display strings.
#[derive(Debug, Clone, Copy)]
pub struct WeeklyGoalDef {
    pub id: &'static str,
    pub description: &'static str,
    pub target: u32,
    pub kind: WeeklyGoalKind,
}

/// Runtime snapshot of game metrics used to evaluate weekly goal progress.
#[derive(Debug, Clone)]
pub struct WeeklyGoalContext {
    pub time_seconds: u64,
    pub used_undo: bool,
    pub draw_mode: DrawStockConfig,
}

impl WeeklyGoalDef {
    /// Returns `true` if this win event counts as one tick of progress
    /// toward this goal.
    pub fn matches(&self, ctx: &WeeklyGoalContext) -> bool {
        match self.kind {
            WeeklyGoalKind::WinGame => true,
            WeeklyGoalKind::WinWithoutUndo => !ctx.used_undo,
            WeeklyGoalKind::WinUnder { seconds } => ctx.time_seconds < seconds,
            WeeklyGoalKind::WinDrawThree => ctx.draw_mode == DrawStockConfig::DrawThree,
        }
    }
}

/// All currently-active weekly goals.
pub const WEEKLY_GOALS: &[WeeklyGoalDef] = &[
    WeeklyGoalDef {
        id: "weekly_5_wins",
        description: "Win 5 games this week",
        target: 5,
        kind: WeeklyGoalKind::WinGame,
    },
    WeeklyGoalDef {
        id: "weekly_3_no_undo",
        description: "Win 3 games without undo this week",
        target: 3,
        kind: WeeklyGoalKind::WinWithoutUndo,
    },
    WeeklyGoalDef {
        id: "weekly_3_fast",
        description: "Win 3 games in under 3 minutes this week",
        target: 3,
        kind: WeeklyGoalKind::WinUnder { seconds: 180 },
    },
    WeeklyGoalDef {
        id: "weekly_1_under_five",
        description: "Win 1 game in under 5 minutes this week",
        target: 1,
        kind: WeeklyGoalKind::WinUnder { seconds: 300 },
    },
    WeeklyGoalDef {
        id: "weekly_draw_three",
        description: "Win 1 Draw-3 game this week",
        target: 1,
        kind: WeeklyGoalKind::WinDrawThree,
    },
];

/// Stable identifier for the ISO week containing `date`, e.g. `"2026-W17"`.
/// Same string for every player worldwide on the same calendar week.
pub fn current_iso_week_key(date: NaiveDate) -> String {
    let iso = date.iso_week();
    format!("{}-W{:02}", iso.year(), iso.week())
}

/// Look up a weekly-goal definition by id.
pub fn weekly_goal_by_id(id: &str) -> Option<&'static WeeklyGoalDef> {
    WEEKLY_GOALS.iter().find(|g| g.id == id)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn ctx(time: u64, undo: bool) -> WeeklyGoalContext {
        WeeklyGoalContext {
            time_seconds: time,
            used_undo: undo,
            draw_mode: DrawStockConfig::DrawOne,
        }
    }

    fn ctx_d3(time: u64) -> WeeklyGoalContext {
        WeeklyGoalContext {
            time_seconds: time,
            used_undo: false,
            draw_mode: DrawStockConfig::DrawThree,
        }
    }

    #[test]
    fn all_goal_ids_are_unique() {
        let mut ids: Vec<&str> = WEEKLY_GOALS.iter().map(|g| g.id).collect();
        ids.sort();
        let len = ids.len();
        ids.dedup();
        assert_eq!(ids.len(), len);
    }

    #[test]
    fn win_game_always_matches() {
        let g = weekly_goal_by_id("weekly_5_wins").unwrap();
        assert!(g.matches(&ctx(60, false)));
        assert!(g.matches(&ctx(99999, true)));
    }

    #[test]
    fn no_undo_only_matches_clean_wins() {
        let g = weekly_goal_by_id("weekly_3_no_undo").unwrap();
        assert!(g.matches(&ctx(120, false)));
        assert!(!g.matches(&ctx(120, true)));
    }

    #[test]
    fn fast_only_matches_under_3_minutes() {
        let g = weekly_goal_by_id("weekly_3_fast").unwrap();
        assert!(g.matches(&ctx(60, true)));
        assert!(g.matches(&ctx(179, true)));
        assert!(!g.matches(&ctx(180, true)));
        assert!(!g.matches(&ctx(300, false)));
    }

    #[test]
    fn iso_week_key_is_stable_within_a_week() {
        let monday = NaiveDate::from_ymd_opt(2026, 4, 20).unwrap(); // 2026-W17 Mon
        let sunday = NaiveDate::from_ymd_opt(2026, 4, 26).unwrap(); // 2026-W17 Sun
        assert_eq!(current_iso_week_key(monday), current_iso_week_key(sunday));
    }

    #[test]
    fn iso_week_key_differs_across_weeks() {
        let w17 = NaiveDate::from_ymd_opt(2026, 4, 20).unwrap();
        let w18 = NaiveDate::from_ymd_opt(2026, 4, 27).unwrap();
        assert_ne!(current_iso_week_key(w17), current_iso_week_key(w18));
    }

    #[test]
    fn iso_week_key_format_includes_year_and_week() {
        let d = NaiveDate::from_ymd_opt(2026, 4, 20).unwrap();
        let key = current_iso_week_key(d);
        assert!(key.starts_with("2026-W"));
        assert_eq!(key.len(), 8);
    }

    #[test]
    fn under_five_matches_wins_below_300_seconds() {
        let g = weekly_goal_by_id("weekly_1_under_five").unwrap();
        assert!(g.matches(&ctx(0, false)));
        assert!(g.matches(&ctx(299, true)));
        assert!(!g.matches(&ctx(300, false)));
        assert!(!g.matches(&ctx(999, false)));
    }

    #[test]
    fn draw_three_goal_matches_only_draw_three_wins() {
        let g = weekly_goal_by_id("weekly_draw_three").unwrap();
        assert!(g.matches(&ctx_d3(600)));
        assert!(!g.matches(&ctx(600, false)));
    }
}
