//! FIFA 17 **non-player content taxonomy** — the evidence-based map from a card
//! `cardsubtypeid` to its functional family (consumables) or role (staff).
//!
//! This is the ONLY place the FIFA-specific `cardsubtypeid` vocabulary lives; it
//! keeps that game concept out of generic Core, exactly as the player-side
//! catalog keeps `resourceId`/`rareflag` out of Core. Nothing here is guessed:
//!
//! * Consumable families and their contiguous `cardsubtypeid` ranges are taken
//!   verbatim from `fifa17-recon/tools/fut_consumables.py`
//!   (`BY_SUBTYPE`/`CORE_KINDS`, Ghidra-derived from `FUN_18013f4d0` /
//!   `FUN_1801bfac0`) and `docs/CARD_TAXONOMY.md` (verified against the `.105`
//!   `fcc_*.json` tables).
//! * Staff roles are the `FUN_1800d8330` family selector: 4=manager, 5=headcoach,
//!   6=gkcoach, 7=physio, 8=fitnesscoach.
//!
//! Display **labels are functional, never marketing** (e.g. "Player Chemistry
//! Style", not a promo name). A `cardsubtypeid` outside every documented range
//! resolves to `None` — the caller DEFERS it (mirroring the player NoName gate),
//! never fabricating a family.

/// The disjoint content classes a FIFA 17 owned card can belong to. Player is
/// the default so a catalog authored before this taxonomy existed (no `kind`
/// field) still classifies every entry as a player, unchanged.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ContentKind {
    #[default]
    Player,
    Consumable,
    Staff,
}

impl ContentKind {
    /// The stable wire/catalog token for this kind.
    pub fn as_str(&self) -> &'static str {
        match self {
            ContentKind::Player => "player",
            ContentKind::Consumable => "consumable",
            ContentKind::Staff => "staff",
        }
    }

    /// Parse a catalog `kind` token. Unknown or "player" (or an absent field that
    /// deserializes to the default) is `Player` — backward compatible.
    // Intentionally infallible (every input maps to a kind, unknown → Player), so
    // it is NOT `std::str::FromStr` (which is fallible); the name mirrors the
    // catalog token vocabulary.
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(s: &str) -> ContentKind {
        match s {
            "consumable" => ContentKind::Consumable,
            "staff" => ContentKind::Staff,
            _ => ContentKind::Player,
        }
    }
}

/// The functional family + honest display label for a consumable `cardsubtypeid`,
/// or `None` if the subtype is outside every documented range (→ DEFER).
///
/// Returns `(family, label)`, both `'static`. `family` is the neutral machine
/// name stored as the CardDefinition family; `label` is the functional
/// human-readable name.
pub fn consumable_family(subtype: i64) -> Option<(&'static str, &'static str)> {
    let pair = match subtype {
        51..=57 => ("gk_training", "GK Training"),
        61..=67 => ("player_training", "Player Training"),
        71..=86 => ("manager_formation_mod", "Manager Formation"),
        91..=110 => ("position_mod", "Position Modifier"),
        121..=136 => ("formation_mod", "Formation Modifier"),
        201 => ("player_contract", "Player Contract"),
        202 => ("manager_contract", "Manager Contract"),
        211..=218 => ("healing", "Healing"),
        219 => ("player_fitness", "Player Fitness"),
        220 => ("squad_fitness", "Squad Fitness"),
        250..=268 => ("player_playstyle", "Player Chemistry Style"),
        269..=273 => ("gk_playstyle", "GK Chemistry Style"),
        300..=341 => ("manager_league", "Manager League Modifier"),
        _ => return None,
    };
    Some(pair)
}

/// The staff role + honest display label for a staff `cardsubtypeid` (4..=8), or
/// `None` for any other subtype (→ DEFER). Grounded in the `FUN_1800d8330`
/// family selector.
pub fn staff_role(subtype: i64) -> Option<(&'static str, &'static str)> {
    let pair = match subtype {
        4 => ("manager", "Manager"),
        5 => ("headcoach", "Head Coach"),
        6 => ("gkcoach", "GK Coach"),
        7 => ("physio", "Physio"),
        8 => ("fitnesscoach", "Fitness Coach"),
        _ => return None,
    };
    Some(pair)
}

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

    #[test]
    fn content_kind_round_trips_and_defaults_to_player() {
        assert_eq!(ContentKind::default(), ContentKind::Player);
        for k in [
            ContentKind::Player,
            ContentKind::Consumable,
            ContentKind::Staff,
        ] {
            assert_eq!(ContentKind::from_str(k.as_str()), k);
        }
        // Unknown / absent tokens fall back to Player (backward compatible).
        assert_eq!(ContentKind::from_str(""), ContentKind::Player);
        assert_eq!(ContentKind::from_str("nonsense"), ContentKind::Player);
        assert_eq!(ContentKind::from_str("player"), ContentKind::Player);
    }

    #[test]
    fn consumable_family_range_boundaries() {
        // Each contiguous range: lower boundary, upper boundary, family + label.
        let cases: &[(i64, i64, &str, &str)] = &[
            (51, 57, "gk_training", "GK Training"),
            (61, 67, "player_training", "Player Training"),
            (71, 86, "manager_formation_mod", "Manager Formation"),
            (91, 110, "position_mod", "Position Modifier"),
            (121, 136, "formation_mod", "Formation Modifier"),
            (211, 218, "healing", "Healing"),
            (250, 268, "player_playstyle", "Player Chemistry Style"),
            (269, 273, "gk_playstyle", "GK Chemistry Style"),
            (300, 341, "manager_league", "Manager League Modifier"),
        ];
        for &(lo, hi, family, label) in cases {
            assert_eq!(consumable_family(lo), Some((family, label)), "lo {lo}");
            assert_eq!(consumable_family(hi), Some((family, label)), "hi {hi}");
        }
        // Singleton subtypes.
        assert_eq!(
            consumable_family(201),
            Some(("player_contract", "Player Contract"))
        );
        assert_eq!(
            consumable_family(202),
            Some(("manager_contract", "Manager Contract"))
        );
        assert_eq!(
            consumable_family(219),
            Some(("player_fitness", "Player Fitness"))
        );
        assert_eq!(
            consumable_family(220),
            Some(("squad_fitness", "Squad Fitness"))
        );
    }

    #[test]
    fn consumable_family_gaps_and_out_of_range_are_none() {
        // Just outside range edges, and in documented gaps between ranges.
        for s in [
            0, 50, 58, 60, 68, 70, 87, 90, 111, 120, 137, 200, 203, 210, 221, 249, 274, 299, 342,
            999,
        ] {
            assert_eq!(consumable_family(s), None, "subtype {s} must be unknown");
        }
    }

    #[test]
    fn staff_role_each_role_and_unknown_is_none() {
        assert_eq!(staff_role(4), Some(("manager", "Manager")));
        assert_eq!(staff_role(5), Some(("headcoach", "Head Coach")));
        assert_eq!(staff_role(6), Some(("gkcoach", "GK Coach")));
        assert_eq!(staff_role(7), Some(("physio", "Physio")));
        assert_eq!(staff_role(8), Some(("fitnesscoach", "Fitness Coach")));
        for s in [0, 1, 2, 3, 9, 10, 201, 300] {
            assert_eq!(staff_role(s), None, "staff subtype {s} must be unknown");
        }
    }
}
