//! Guarded config-reload primitive (ADR-033 `config-watcher-applies-guard`).
//!
//! When the daemon detects a change to `.ontoref/config.ncl::ops.tier` (via
//! a future file-watching surface or an HTTP endpoint), it MUST consult
//! [`crate::tier_guard::enforce_tier_transition_guard`] before swapping
//! the new ops value into any in-process state. This module provides the
//! pure decision function the watcher invokes — same shape as
//! `tier_guard` itself: no IO, deterministic, fully tested.
//!
//! The reload mechanism (file notify → reparse → invoke this function) is
//! NOT in this module. That integration lives in the eventual config.ncl
//! watcher. This file pins the semantic contract: every reload path
//! consults the guard, and the function returns one of two outcomes:
//!
//! - `ReloadAction::Apply(new)`  — caller swaps registry/state in.
//! - `ReloadAction::Block { .. }` — caller logs `Block`-severity notification
//!   and retains the previous config; the file on disk is NOT mutated (per
//!   ADR-033: the daemon only refuses to honour the edit, never overwrites the
//!   user's NCL).

use crate::tier::OpsConfig;
use crate::tier_guard::{enforce_tier_transition_guard, TierGuardError};

/// What the reload caller should do after the guard has run.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReloadAction {
    /// The new config may be swapped in. Carries the validated new ops
    /// so the caller does not need to re-compute it.
    Apply(OpsConfig),
    /// The new config must NOT be applied. Caller should log a
    /// `Block`-severity notification using `reason` and retain the
    /// previous in-memory ops value.
    Block {
        /// Human-readable explanation for telemetry / Block notification.
        reason: String,
        /// Pending-migration ids that caused the rejection.
        ids: Vec<String>,
    },
}

/// Evaluate whether a detected `.ontoref/config.ncl` change is permitted
/// to swap the in-memory `OpsConfig`. Returns `Apply` when the guard
/// accepts the transition, `Block` when migrations are pending and the
/// tier actually changed.
///
/// The `Apply` outcome also fires for same-tier reloads (e.g. a phase
/// change or unrelated config edit that left `ops.tier` untouched) —
/// the guard treats those as no-op transitions per `tier_guard`'s own
/// behaviour matrix.
pub fn apply_config_reload(
    old: &OpsConfig,
    new: &OpsConfig,
    pending_migrations: &[String],
) -> ReloadAction {
    match enforce_tier_transition_guard(old, new, pending_migrations) {
        Ok(()) => ReloadAction::Apply(new.clone()),
        Err(TierGuardError::MigrationsPending {
            old_tier,
            new_tier,
            pending,
            ids,
        }) => ReloadAction::Block {
            reason: format!(
                "tier change {old_tier:?} → {new_tier:?} refused — {pending} migration(s) pending"
            ),
            ids,
        },
    }
}

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

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

    #[test]
    fn same_tier_reload_is_apply() {
        let old = ops(Tier::Tier1);
        let new = ops(Tier::Tier1);
        assert!(matches!(
            apply_config_reload(&old, &new, &[]),
            ReloadAction::Apply(_)
        ));
    }

    #[test]
    fn same_tier_reload_with_pending_migrations_is_apply() {
        let old = ops(Tier::Tier1);
        let new = ops(Tier::Tier1);
        let pending = vec!["0023".to_string()];
        match apply_config_reload(&old, &new, &pending) {
            ReloadAction::Apply(applied) => assert_eq!(applied.tier, Tier::Tier1),
            other => panic!("expected Apply, got {other:?}"),
        }
    }

    #[test]
    fn clean_tier_transition_is_apply() {
        let old = ops(Tier::Tier0);
        let new = ops(Tier::Tier2);
        match apply_config_reload(&old, &new, &[]) {
            ReloadAction::Apply(applied) => assert_eq!(applied.tier, Tier::Tier2),
            other => panic!("expected Apply, got {other:?}"),
        }
    }

    #[test]
    fn tier_change_with_pending_migrations_is_block() {
        let old = ops(Tier::Tier0);
        let new = ops(Tier::Tier1);
        let pending = vec!["0023".to_string(), "0024".to_string()];
        match apply_config_reload(&old, &new, &pending) {
            ReloadAction::Block { reason, ids } => {
                assert!(reason.contains("Tier0"));
                assert!(reason.contains("Tier1"));
                assert!(reason.contains("2 migration"));
                assert_eq!(ids, vec!["0023".to_string(), "0024".to_string()]);
            }
            other => panic!("expected Block, got {other:?}"),
        }
    }

    #[test]
    fn downgrade_with_pending_migrations_is_also_block() {
        let old = ops(Tier::Tier2);
        let new = ops(Tier::Tier0);
        let pending = vec!["0023".to_string()];
        assert!(matches!(
            apply_config_reload(&old, &new, &pending),
            ReloadAction::Block { .. }
        ));
    }

    #[test]
    fn block_outcome_carries_migration_ids_verbatim() {
        let old = ops(Tier::Tier0);
        let new = ops(Tier::Tier1);
        let pending = vec!["0022".to_string(), "0025".to_string(), "0027".to_string()];
        match apply_config_reload(&old, &new, &pending) {
            ReloadAction::Block { ids, .. } => assert_eq!(ids, pending),
            other => panic!("expected Block, got {other:?}"),
        }
    }
}
