//! Tier-transition guard (ADR-029 Hard constraint
//! `tier-transition-requires-clean-migration-state`).
//!
//! Pure function that refuses tier mutation when migrations are pending.
//! Designed as a sub-component within the broader config-watching surface
//! (per ADR-029 / question 3-C of the 2026-05-26 session): single
//! file-watcher infrastructure, logic separated into a testable function.
//!
//! Wiring: the runtime caller (config reload path, or future
//! `transition_tier` op pre-validator) invokes
//! [`enforce_tier_transition_guard`] with the OLD and NEW config plus the
//! current pending-migration list. The function returns `Ok(())` when the
//! transition is permitted and [`TierGuardError::MigrationsPending`] when
//! it is not. The caller is responsible for emitting the
//! `Block`-severity notification and refusing to swap the config in the
//! registry.

use crate::tier::{OpsConfig, Tier};

/// Failure mode emitted by the guard.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum TierGuardError {
    /// The new config changes `ops.tier` but pending migrations exist.
    /// Callers MUST refuse to apply the new config until the migration
    /// baseline is clean.
    #[error(
        "tier transition {old_tier:?} -> {new_tier:?} blocked: {pending} migration(s) pending: \
         {ids:?}"
    )]
    MigrationsPending {
        /// Tier value declared by the OLD config (or default if absent).
        old_tier: Tier,
        /// Tier value declared by the NEW config.
        new_tier: Tier,
        /// Number of pending migrations.
        pending: usize,
        /// Ids of pending migrations (e.g. `["0022", "0023"]`).
        ids: Vec<String>,
    },
}

/// Pure check: returns `Ok(())` when the config transition is permitted,
/// `Err(MigrationsPending)` when `ops.tier` changed while migrations are
/// pending.
///
/// Behaviour matrix:
///
/// | `old.tier == new.tier` | `pending_migrations.is_empty()` | result |
/// |---|---|---|
/// | yes | yes | `Ok` |
/// | yes | no  | `Ok` (no transition, migrations are unrelated) |
/// | no  | yes | `Ok` (clean transition) |
/// | no  | no  | `Err(MigrationsPending)` |
///
/// Note: the function does NOT check `ops.phase` — Phase transitions
/// (`Progressive` → `Pure` within tier-2) are governed by a separate
/// constraint (future ADR-033). This guard is scoped to tier transitions
/// only, matching the ADR-029 /
/// `tier-transition-requires-clean-migration-state` constraint precisely.
pub fn enforce_tier_transition_guard(
    old: &OpsConfig,
    new: &OpsConfig,
    pending_migrations: &[String],
) -> Result<(), TierGuardError> {
    if old.tier == new.tier {
        return Ok(());
    }
    if pending_migrations.is_empty() {
        return Ok(());
    }
    Err(TierGuardError::MigrationsPending {
        old_tier: old.tier,
        new_tier: new.tier,
        pending: pending_migrations.len(),
        ids: pending_migrations.to_vec(),
    })
}

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

    fn cfg(tier: Tier) -> OpsConfig {
        OpsConfig {
            tier,
            phase: Phase::Progressive,
        }
    }

    #[test]
    fn unchanged_tier_with_no_migrations_pending_is_ok() {
        let result = enforce_tier_transition_guard(&cfg(Tier::Tier0), &cfg(Tier::Tier0), &[]);
        assert!(result.is_ok());
    }

    #[test]
    fn unchanged_tier_with_migrations_pending_is_ok() {
        // Migrations pending but no tier change — guard does not fire.
        let result = enforce_tier_transition_guard(
            &cfg(Tier::Tier1),
            &cfg(Tier::Tier1),
            &["0023".to_string()],
        );
        assert!(result.is_ok());
    }

    #[test]
    fn upgrade_with_clean_migrations_is_ok() {
        let result = enforce_tier_transition_guard(&cfg(Tier::Tier0), &cfg(Tier::Tier2), &[]);
        assert!(result.is_ok());
    }

    #[test]
    fn downgrade_with_clean_migrations_is_ok() {
        let result = enforce_tier_transition_guard(&cfg(Tier::Tier2), &cfg(Tier::Tier0), &[]);
        assert!(result.is_ok());
    }

    #[test]
    fn upgrade_with_migrations_pending_is_blocked() {
        let pending = vec!["0022".to_string(), "0023".to_string()];
        let result = enforce_tier_transition_guard(&cfg(Tier::Tier0), &cfg(Tier::Tier2), &pending);
        match result {
            Err(TierGuardError::MigrationsPending {
                old_tier,
                new_tier,
                pending,
                ids,
            }) => {
                assert_eq!(old_tier, Tier::Tier0);
                assert_eq!(new_tier, Tier::Tier2);
                assert_eq!(pending, 2);
                assert_eq!(ids, vec!["0022".to_string(), "0023".to_string()]);
            }
            other => panic!("expected MigrationsPending, got {other:?}"),
        }
    }

    #[test]
    fn downgrade_with_migrations_pending_is_blocked() {
        let pending = vec!["0023".to_string()];
        let result = enforce_tier_transition_guard(&cfg(Tier::Tier2), &cfg(Tier::Tier1), &pending);
        assert!(matches!(
            result,
            Err(TierGuardError::MigrationsPending { .. })
        ));
    }

    #[test]
    fn adjacent_tier_change_blocked_when_migration_pending() {
        // The constraint applies to ANY tier change, not only multi-step.
        let pending = vec!["0023".to_string()];
        let result = enforce_tier_transition_guard(&cfg(Tier::Tier0), &cfg(Tier::Tier1), &pending);
        assert!(matches!(
            result,
            Err(TierGuardError::MigrationsPending { .. })
        ));
    }

    #[test]
    fn error_displays_migration_ids() {
        let err = TierGuardError::MigrationsPending {
            old_tier: Tier::Tier0,
            new_tier: Tier::Tier2,
            pending: 2,
            ids: vec!["0022".to_string(), "0023".to_string()],
        };
        let msg = err.to_string();
        assert!(msg.contains("Tier0"));
        assert!(msg.contains("Tier2"));
        assert!(msg.contains("0022"));
        assert!(msg.contains("0023"));
    }
}
