//! 5650 — Adjudication layer: the permission-tier vocabulary.
//!
//! Every `EventKind` is gated by a [`PermissionTier`]. Every seat
//! holds a [`crate::config::SeatRole`]. When a player proposes an
//! event, the engine checks tier against role at `propose()` entry
//! and routes violations through D27 `EnforcementMode`.
//!
//! This module supersedes the deleted `creation_policy` and
//! `sheet_edit_policy` engine handlers — both bypass D27. Tiered
//! data permissions unify them.
//!
//! See `.backlog/deployed/5650-game-model-adjudication-layer.md`.

use crate::config::SeatRole;
use crate::types::SeatId;

/// Per-`EventKind` permission gate. The engine looks up the tier
/// for the proposed event, then evaluates [`tier_permits`] against
/// the proposer's [`SeatRole`] and the target entity's owner.
///
/// The variants are NOT a linear order — `OwnerOrGm` is conditional
/// on the target entity's ownership. Use [`tier_permits`] to evaluate.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PermissionTier {
    /// Anyone — including Observer-tier seats — may propose. Reserved
    /// for cosmetic / spectator-safe events. Rare.
    Free,

    /// Player-tier or Gm-tier seats may propose. Observer / Arbiter
    /// blocked. No ownership check.
    AnyPlayer,

    /// Gm-tier seats may always propose; Player-tier seats may
    /// propose only if `target_owner == Some(by_seat)`. Observers
    /// blocked.
    ///
    /// This is the default for most entity-mutation events
    /// (`SheetEdited`, `Moved`, `PoolChanged`).
    #[default]
    OwnerOrGm,

    /// Gm-tier seats only. Reserved for session-administration
    /// events (creation, phase advancement, turn order changes).
    GmOnly,

    /// Nobody may propose. Always routes through D27 — engine
    /// emits this for events that exist solely to surface
    /// adjudication prompts.
    Locked,
}

/// Returns `true` iff the proposer with `role` is permitted to
/// propose an event with the given `tier`. `target_owner` is the
/// owning seat of the event's primary target entity, if any
/// (extracted via the engine's `event_target_owner` helper).
///
/// - [`PermissionTier::Free`] permits any seat.
/// - [`PermissionTier::AnyPlayer`] permits Player or any
///   cross-actor-authority role (`Sysadmin`/`Gamemaster`).
/// - [`PermissionTier::OwnerOrGm`] permits cross-actor roles
///   unconditionally; permits Player only when `target_owner == Some(by_seat)`.
/// - [`PermissionTier::GmOnly`] permits cross-actor roles only.
/// - [`PermissionTier::Locked`] never permits.
pub fn tier_permits(
    tier: PermissionTier,
    role: SeatRole,
    target_owner: Option<SeatId>,
    by_seat: SeatId,
) -> bool {
    if role.is_observer() {
        // Observer / Arbiter never propose, regardless of tier (even
        // Free) — Observer-tier is read-only by definition.
        return matches!(tier, PermissionTier::Free);
    }
    let is_gm_tier = role.has_cross_actor_authority();
    match tier {
        PermissionTier::Free => true,
        PermissionTier::AnyPlayer => true, // role is not Observer (checked above)
        PermissionTier::OwnerOrGm => {
            is_gm_tier || target_owner == Some(by_seat)
        }
        PermissionTier::GmOnly => is_gm_tier,
        PermissionTier::Locked => false,
    }
}

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

    const SEAT_0: SeatId = SeatId::from_raw(0);
    const SEAT_1: SeatId = SeatId::from_raw(1);

    // ----- Free -----
    #[test]
    fn free_permits_player_and_gm() {
        assert!(tier_permits(PermissionTier::Free, SeatRole::Player, None, SEAT_0));
        assert!(tier_permits(PermissionTier::Free, SeatRole::Gamemaster, None, SEAT_0));
        assert!(tier_permits(PermissionTier::Free, SeatRole::Sysadmin, None, SEAT_0));
    }

    #[test]
    fn free_permits_observer() {
        // Observer-tier exception: Free is the one tier that lets
        // observers act. They still can't propose most things.
        assert!(tier_permits(PermissionTier::Free, SeatRole::Observer, None, SEAT_0));
    }

    // ----- AnyPlayer -----
    #[test]
    fn any_player_permits_player_and_gm_blocks_observer() {
        assert!(tier_permits(PermissionTier::AnyPlayer, SeatRole::Player, None, SEAT_0));
        assert!(tier_permits(PermissionTier::AnyPlayer, SeatRole::Gamemaster, None, SEAT_0));
        assert!(!tier_permits(PermissionTier::AnyPlayer, SeatRole::Observer, None, SEAT_0));
        assert!(!tier_permits(PermissionTier::AnyPlayer, SeatRole::Arbiter, None, SEAT_0));
    }

    // ----- OwnerOrGm -----
    #[test]
    fn owner_or_gm_player_with_matching_owner_permits() {
        assert!(tier_permits(PermissionTier::OwnerOrGm, SeatRole::Player, Some(SEAT_0), SEAT_0));
    }

    #[test]
    fn owner_or_gm_player_with_other_owner_denies() {
        assert!(!tier_permits(PermissionTier::OwnerOrGm, SeatRole::Player, Some(SEAT_1), SEAT_0));
    }

    #[test]
    fn owner_or_gm_player_with_no_owner_denies() {
        assert!(!tier_permits(PermissionTier::OwnerOrGm, SeatRole::Player, None, SEAT_0));
    }

    #[test]
    fn owner_or_gm_gm_always_permits() {
        assert!(tier_permits(PermissionTier::OwnerOrGm, SeatRole::Gamemaster, None, SEAT_0));
        assert!(tier_permits(PermissionTier::OwnerOrGm, SeatRole::Gamemaster, Some(SEAT_1), SEAT_0));
        assert!(tier_permits(PermissionTier::OwnerOrGm, SeatRole::Sysadmin, Some(SEAT_1), SEAT_0));
    }

    #[test]
    fn owner_or_gm_blocks_observer() {
        assert!(!tier_permits(PermissionTier::OwnerOrGm, SeatRole::Observer, Some(SEAT_0), SEAT_0));
    }

    // ----- GmOnly -----
    #[test]
    fn gm_only_blocks_player_permits_gm() {
        assert!(!tier_permits(PermissionTier::GmOnly, SeatRole::Player, None, SEAT_0));
        assert!(tier_permits(PermissionTier::GmOnly, SeatRole::Gamemaster, None, SEAT_0));
        assert!(tier_permits(PermissionTier::GmOnly, SeatRole::Sysadmin, None, SEAT_0));
    }

    // ----- Locked -----
    #[test]
    fn locked_blocks_everyone() {
        assert!(!tier_permits(PermissionTier::Locked, SeatRole::Player, None, SEAT_0));
        assert!(!tier_permits(PermissionTier::Locked, SeatRole::Gamemaster, None, SEAT_0));
        assert!(!tier_permits(PermissionTier::Locked, SeatRole::Sysadmin, None, SEAT_0));
        assert!(!tier_permits(PermissionTier::Locked, SeatRole::Observer, None, SEAT_0));
    }
}
