//! Shape OpenFUT Core's semantic owned inventory into the FIFA 17 `/club`
//! response envelope `{"itemData":[ <player item>, … ]}`.
//!
//! This module owns only the **`/club` envelope**; the per-item shape lives in
//! the shared [`crate::fut::item`] primitive so `/club` and squad projection
//! emit byte-identical items. Items whose real FIFA asset id is unknown are
//! **dropped and counted** here (a collection may omit an unrenderable card);
//! squad projection, which cannot omit a starter, refuses instead.

use serde_json::{json, Value};

use crate::fut::entities::ReverseEntityResolver;
use crate::fut::item::shape_item;
// Re-exported so existing `club_response::{…}` callers keep working; the types
// are now defined once in `fut::item`.
pub use crate::fut::item::{CoreOwnedItem, Fifa17Identity, ItemIdentityResolver, ShapeStats};

/// Shape the whole `/club` response. Items without a resolvable real asset id
/// are dropped (counted in `ShapeStats`), never emitted with a fabricated id.
pub fn shape_club_response<I: ItemIdentityResolver + ?Sized>(
    items: &[CoreOwnedItem],
    ent: &impl ReverseEntityResolver,
    ident: &I,
) -> (Value, ShapeStats) {
    let mut out = Vec::with_capacity(items.len());
    let mut stats = ShapeStats::default();
    for item in items {
        match ident.resolve(item) {
            Some(id) => {
                out.push(shape_item(item, id, ent));
                stats.emitted += 1;
            }
            None => stats.dropped_no_asset += 1,
        }
    }
    (json!({ "itemData": out }), stats)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fut::entities::Fifa17Entities;
    use std::collections::HashMap;

    fn entities() -> Fifa17Entities {
        Fifa17Entities::from_maps(
            HashMap::from([(13, "Premier League".to_string())]),
            HashMap::from([(52, "Argentina".to_string())]),
            HashMap::from([(5, "Chelsea".to_string())]),
        )
    }

    fn item(
        owned: &str,
        card: &str,
        rating: u8,
        pos: &str,
        nation: &str,
        league: &str,
        club: &str,
    ) -> CoreOwnedItem {
        CoreOwnedItem {
            owned_card_id: owned.into(),
            card_id: card.into(),
            rating,
            position: pos.into(),
            nation: nation.into(),
            league: league.into(),
            club: club.into(),
            attributes: [90, 88, 70, 85, 40, 78],
        }
    }

    /// Test resolver: card_id -> real asset id, item_id from a table. Stands in
    /// for the (unresolved-in-production) Core-card→asset mapping.
    struct MapIdentity(HashMap<String, Fifa17Identity>);
    impl ItemIdentityResolver for MapIdentity {
        fn resolve(&self, it: &CoreOwnedItem) -> Option<Fifa17Identity> {
            self.0.get(&it.card_id).copied()
        }
    }

    #[test]
    fn shapes_item_with_full_field_set_and_reverse_ids() {
        let ent = entities();
        let ident = MapIdentity(HashMap::from([(
            "card_ch_1".to_string(),
            Fifa17Identity {
                item_id: 100000001,
                asset_id: 20801,
                resource_id: 20801,
                rareflag: 1,
            },
        )]));
        let items = vec![item(
            "oc1",
            "card_ch_1",
            86,
            "CDM",
            "Argentina",
            "Premier League",
            "Chelsea",
        )];
        let (body, stats) = shape_club_response(&items, &ent, &ident);
        assert_eq!(stats.emitted, 1);
        assert_eq!(stats.dropped_no_asset, 0);
        let it = &body["itemData"][0];
        assert_eq!(it["id"], 100000001);
        assert_eq!(it["resourceId"], 20801);
        assert_eq!(it["assetId"], 20801);
        assert_eq!(
            it["definitionId"], 20801,
            "version byte 0 => resourceId==assetId==definitionId"
        );
        assert_eq!(it["rating"], 86);
        assert_eq!(it["preferredPosition"], "CDM");
        assert_eq!(it["leagueId"], 13);
        assert_eq!(it["teamid"], 5);
        assert_eq!(it["nation"], 52);
        assert_eq!(it["itemType"], "player");
        assert_eq!(it["rareflag"], 1);
        assert_eq!(it["contract"], 7);
        assert_eq!(it["fitness"], 99);
        assert_eq!(it["attributeList"].as_array().unwrap().len(), 6);
        assert_eq!(it["attributeList"][0], json!({"index":0,"value":90}));
    }

    #[test]
    fn drops_items_without_a_real_asset_id_never_faking() {
        let ent = entities();
        // Empty identity map == the current synthetic-catalogue reality.
        let ident = MapIdentity(HashMap::new());
        let items = vec![item(
            "oc1",
            "card_pl_001",
            84,
            "ST",
            "England",
            "Premier League",
            "Northgate United",
        )];
        let (body, stats) = shape_club_response(&items, &ent, &ident);
        assert_eq!(stats.emitted, 0);
        assert_eq!(stats.dropped_no_asset, 1);
        assert_eq!(
            body["itemData"].as_array().unwrap().len(),
            0,
            "no fabricated ids emitted"
        );
    }

    #[test]
    fn unresolved_entity_names_become_neutral_zero_not_dropped() {
        let ent = entities();
        let ident = MapIdentity(HashMap::from([(
            "card_x".to_string(),
            Fifa17Identity {
                item_id: 100000002,
                asset_id: 158023,
                resource_id: 158023,
                rareflag: 1,
            },
        )]));
        // Synthetic club "Northgate United" has no FIFA team id.
        let items = vec![item(
            "oc2",
            "card_x",
            84,
            "ST",
            "England",
            "Premier League",
            "Northgate United",
        )];
        let (body, _) = shape_club_response(&items, &ent, &ident);
        let it = &body["itemData"][0];
        assert_eq!(
            it["teamid"], 0,
            "unknown club -> neutral 0, item still emitted"
        );
        assert_eq!(it["leagueId"], 13);
        assert_eq!(it["nation"], 0, "England not in the test nation map -> 0");
    }

    #[test]
    fn envelope_is_itemdata_object() {
        let ent = entities();
        let ident = MapIdentity(HashMap::new());
        let (body, _) = shape_club_response(&[], &ent, &ident);
        assert!(body.get("itemData").unwrap().is_array());
        assert_eq!(
            body.as_object().unwrap().len(),
            1,
            "only itemData at top level"
        );
    }
}
