//! Pure serialisation logic for the `timers.json` persistence format.
//!
//! No side effects: no filesystem access, no clock reads.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

// ── Data types ─────────────────────────────────────────────────────────────

/// A single scheduled timer entry.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TimerEntry {
    /// The full story ID (filename stem, e.g. `421_story_foo`).
    pub story_id: String,
    /// UTC instant at which the timer should fire.
    pub scheduled_at: DateTime<Utc>,
}

// ── Serialisation ──────────────────────────────────────────────────────────

/// Serialise a slice of timer entries to a pretty-printed JSON string.
pub(super) fn serialize_timers(entries: &[TimerEntry]) -> Result<String, String> {
    serde_json::to_string_pretty(entries).map_err(|e| format!("Serialize failed: {e}"))
}

/// Deserialise a JSON string into a vector of timer entries.
/// Returns `None` if the input cannot be parsed.
pub(super) fn deserialize_timers(s: &str) -> Option<Vec<TimerEntry>> {
    serde_json::from_str(s).ok()
}

// ── Tests ──────────────────────────────────────────────────────────────────

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

    fn make_entry(story_id: &str, secs_from_epoch: i64) -> TimerEntry {
        TimerEntry {
            story_id: story_id.to_string(),
            scheduled_at: Utc.timestamp_opt(secs_from_epoch, 0).unwrap(),
        }
    }

    #[test]
    fn empty_slice_serialises_to_empty_array() {
        let s = serialize_timers(&[]).unwrap();
        assert_eq!(s.trim(), "[]");
    }

    #[test]
    fn single_entry_round_trips() {
        let entry = make_entry("421_story_foo", 1_700_000_000);
        let s = serialize_timers(std::slice::from_ref(&entry)).unwrap();
        let back = deserialize_timers(&s).unwrap();
        assert_eq!(back.len(), 1);
        assert_eq!(back[0], entry);
    }

    #[test]
    fn multiple_entries_round_trip() {
        let entries = vec![
            make_entry("421_story_foo", 1_700_000_000),
            make_entry("422_story_bar", 1_700_001_000),
        ];
        let s = serialize_timers(&entries).unwrap();
        let back = deserialize_timers(&s).unwrap();
        assert_eq!(back, entries);
    }

    #[test]
    fn deserialize_invalid_json_returns_none() {
        assert!(deserialize_timers("not valid json").is_none());
    }

    #[test]
    fn deserialize_wrong_schema_returns_none() {
        assert!(deserialize_timers("[1, 2, 3]").is_none());
    }

    #[test]
    fn story_id_preserved_exactly() {
        let entry = make_entry("615_story_extract_timer_service", 1_700_000_000);
        let s = serialize_timers(std::slice::from_ref(&entry)).unwrap();
        let back = deserialize_timers(&s).unwrap();
        assert_eq!(back[0].story_id, "615_story_extract_timer_service");
    }
}
