// Centralized feature → tier mapping for license entitlements.
//
// This module is the single source of truth for which features are gated by which tier
// on the Rust side. To change what is gated where, modify FEATURE_TIERS only — never
// sprinkle edition checks throughout the codebase.
//
// Since the tier-gating work (#297-#301), UI-level gates (budget, adjustments,
// reports-advanced, multi-profile, balance) live in the TS matrix
// `src/shared/entitlements.ts` (soft-paywall, UI-only enforcement). This table
// only keeps the features actually checked through `check_entitlement` — the
// JWT `features[]` array is a namespace shared with that TS layer, so keys are
// kebab-case on both sides.

/// Editions, ordered from least to most privileged.
pub const EDITION_FREE: &str = "free";
pub const EDITION_BASE: &str = "base";
pub const EDITION_PREMIUM: &str = "premium";

/// Maps feature name → list of editions allowed to use it.
/// A feature absent from this list is denied for all editions, unless the
/// license carries it in its signed `features[]` override (see [`is_entitled`]).
const FEATURE_TIERS: &[(&str, &[&str])] = &[("auto-update", &[EDITION_BASE, EDITION_PREMIUM])];

/// Pure check: does `edition` grant access to `feature` via the static matrix?
pub fn is_feature_allowed(feature: &str, edition: &str) -> bool {
    FEATURE_TIERS
        .iter()
        .find(|(name, _)| *name == feature)
        .map(|(_, tiers)| tiers.contains(&edition))
        .unwrap_or(false)
}

/// Static matrix check OR signed per-license `features[]` override.
///
/// Fail-closed in Free (CWE-863): a `license.key` copied onto another machine
/// resolves to "free" through the machine-binding path, which already drops the
/// signed features (see `license_commands::current_entitlements`). As defense
/// in depth we also refuse the override here whenever the edition is free, so
/// signed features can never rescue a downgraded license.
pub fn is_entitled(feature: &str, edition: &str, features: &[String]) -> bool {
    if edition == EDITION_FREE {
        return is_feature_allowed(feature, edition);
    }
    is_feature_allowed(feature, edition) || features.iter().any(|f| f == feature)
}

/// Tauri command: is `feature` available right now? Edition AND signed
/// per-license feature overrides are resolved through the same machine-binding
/// path (`license_commands::current_entitlements`), then combined by
/// [`is_entitled`].
#[tauri::command]
pub fn check_entitlement(app: tauri::AppHandle, feature: String) -> Result<bool, String> {
    let (edition, features) = crate::commands::license_commands::current_entitlements(&app);
    Ok(is_entitled(&feature, &edition, &features))
}

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

    #[test]
    fn free_denied_auto_update() {
        assert!(!is_feature_allowed("auto-update", EDITION_FREE));
    }

    #[test]
    fn base_unlocks_auto_update() {
        assert!(is_feature_allowed("auto-update", EDITION_BASE));
    }

    #[test]
    fn premium_unlocks_auto_update() {
        assert!(is_feature_allowed("auto-update", EDITION_PREMIUM));
    }

    #[test]
    fn unknown_feature_denied() {
        assert!(!is_feature_allowed("nonexistent", EDITION_PREMIUM));
    }

    #[test]
    fn matrix_grants_without_override() {
        assert!(is_entitled("auto-update", EDITION_BASE, &[]));
        assert!(is_entitled("auto-update", EDITION_PREMIUM, &[]));
    }

    #[test]
    fn unlisted_feature_denied_without_override() {
        // "balance" lives in the TS matrix only — absent from FEATURE_TIERS,
        // so the static path denies it for every edition.
        assert!(!is_entitled("balance", EDITION_BASE, &[]));
        assert!(!is_entitled("balance", EDITION_PREMIUM, &[]));
    }

    #[test]
    fn override_grants_unlisted_feature_for_paid_editions() {
        let features = vec!["balance".to_string()];
        assert!(is_entitled("balance", EDITION_BASE, &features));
        assert!(is_entitled("balance", EDITION_PREMIUM, &features));
    }

    #[test]
    fn override_ignored_in_free() {
        // CWE-863: signed features[] must never rescue a license downgraded to
        // free (copied key / machine mismatch) — not even for a feature that a
        // paid edition would get from the static matrix.
        let features = vec!["balance".to_string(), "auto-update".to_string()];
        assert!(!is_entitled("balance", EDITION_FREE, &features));
        assert!(!is_entitled("auto-update", EDITION_FREE, &features));
    }
}
