//! Theme-store catalog wire types.
//!
//! Shared between `solitaire_server` (which builds the catalog by
//! scanning its theme-store directory) and `solitaire_data` (which
//! fetches it and downloads theme archives). These are **additive**
//! API types: they do not participate in [`crate::SyncPayload`] and
//! changing them cannot break the sync wire format.
//!
//! Store entries describe downloadable card-art theme archives — the
//! same `.zip` format consumed by the engine's theme importer (a
//! `theme.ron` manifest plus 53 SVGs).

use serde::{Deserialize, Serialize};

/// One downloadable theme in the server's catalog.
///
/// URLs are server-relative (e.g. `/api/themes/neon/download`) so a
/// client can join them onto whichever base URL it is configured with.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThemeCatalogEntry {
    /// Unique theme id — `meta.id` from the archive's `theme.ron`.
    /// Also the id the theme installs under on the client.
    pub id: String,
    /// Display name shown in the store UI.
    pub name: String,
    /// Author attribution (free-form text).
    pub author: String,
    /// Version string (conventionally semver).
    pub version: String,
    /// Card aspect ratio as `(numerator, denominator)`.
    pub card_aspect: (u32, u32),
    /// Size of the `.zip` archive in bytes. Clients use this for
    /// display and as a pre-download sanity bound.
    pub size_bytes: u64,
    /// Lowercase-hex SHA-256 of the `.zip` archive. Clients MUST
    /// verify the downloaded bytes against this digest before handing
    /// the archive to the theme importer.
    pub sha256: String,
    /// Server-relative URL of the theme archive.
    pub download_url: String,
    /// Server-relative URL of a PNG preview, when the store ships one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub preview_url: Option<String>,
}

/// Response body of the catalog listing endpoint (`GET /api/themes`).
///
/// Wrapped in a struct (rather than a bare `Vec`) so future additive
/// fields — pagination, store revision, purchase metadata — don't
/// break existing clients.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThemeCatalogResponse {
    /// Every theme currently offered by the server, sorted by name.
    pub themes: Vec<ThemeCatalogEntry>,
}

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

    fn entry() -> ThemeCatalogEntry {
        ThemeCatalogEntry {
            id: "neon".into(),
            name: "Neon".into(),
            author: "Rusty Solitaire".into(),
            version: "1.0.0".into(),
            card_aspect: (2, 3),
            size_bytes: 123_456,
            sha256: "ab".repeat(32),
            download_url: "/api/themes/neon/download".into(),
            preview_url: Some("/api/themes/neon/preview".into()),
        }
    }

    #[test]
    fn catalog_round_trips_through_json() {
        let response = ThemeCatalogResponse {
            themes: vec![entry()],
        };
        let json = serde_json::to_string(&response).expect("serialize");
        let back: ThemeCatalogResponse = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, response);
    }

    #[test]
    fn missing_preview_url_deserializes_to_none() {
        // Older servers (or themes without preview art) omit the key
        // entirely; the field must default rather than error.
        let json = r#"{
            "id": "neon",
            "name": "Neon",
            "author": "Rusty Solitaire",
            "version": "1.0.0",
            "card_aspect": [2, 3],
            "size_bytes": 1,
            "sha256": "00",
            "download_url": "/api/themes/neon/download"
        }"#;
        let entry: ThemeCatalogEntry = serde_json::from_str(json).expect("deserialize");
        assert_eq!(entry.preview_url, None);
    }
}
