//! Staffing task-class rules for the TruthStore.
//!
//! Phase 42 PRD: "Staffing rules ship first. Terraform/Ansible rule
//! shapes are scaffolded but unpopulated until the long-horizon phase."
//! This module owns the staffing rule set; `devops.rs` holds the
//! matching scaffold for the DevOps long-horizon.
//!
//! Rules registered here live under the task classes `staffing.fill`
//! (fill proposals), `staffing.rescue` (rescue escalations), and
//! `staffing.any` (rules that apply across all staffing task classes —
//! PII redaction being the canonical example).
//!
//! All rules are evaluated via the `TruthStore::evaluate` walk, which
//! pairs each rule's `RuleCondition` against a caller-supplied JSON
//! context and emits a `RuleOutcome { passed, action }` per rule.
//! Downstream enforcement (router gate, SQL gate, execution-loop gate)
//! decides how to apply the action — `Reject` / `Block` shortcircuit,
//! `Redact` mutates, `Pass` is informational.

use crate::{RuleAction, RuleCondition, TruthRule, TruthStore};

/// Register the staffing rule set on an existing store. Returns the
/// store for chaining if the caller wants to fold other rule sets on
/// top (e.g. `staffing_rules(devops_rules(TruthStore::new()))`).
pub fn staffing_rules(mut store: TruthStore) -> TruthStore {
    store.add_rule(TruthRule {
        id: "worker-active".to_string(),
        task_class: "staffing.fill".to_string(),
        description: "Worker must be active".to_string(),
        condition: RuleCondition::FieldEquals {
            field: "worker.status".to_string(),
            value: "active".to_string(),
        },
        action: RuleAction::Pass,
    });

    store.add_rule(TruthRule {
        id: "client-not-blacklisted".to_string(),
        task_class: "staffing.fill".to_string(),
        description: "Worker cannot be blacklisted for client".to_string(),
        condition: RuleCondition::FieldEquals {
            field: "worker.client_blacklisted".to_string(),
            value: "false".to_string(),
        },
        action: RuleAction::Pass,
    });

    store.add_rule(TruthRule {
        id: "deadline-required".to_string(),
        task_class: "staffing.fill".to_string(),
        description: "Contract must have deadline".to_string(),
        condition: RuleCondition::FieldEmpty {
            field: "contract.deadline".to_string(),
        },
        action: RuleAction::Reject {
            message: "Contract deadline is required".to_string(),
        },
    });

    store.add_rule(TruthRule {
        id: "budget-required".to_string(),
        task_class: "staffing.fill".to_string(),
        description: "Budget must be non-negative".to_string(),
        condition: RuleCondition::FieldGreater {
            field: "contract.budget_per_hour_max".to_string(),
            threshold: 0,
        },
        action: RuleAction::Pass,
    });

    store.add_rule(TruthRule {
        id: "pii-redact".to_string(),
        task_class: "staffing.any".to_string(),
        description: "Redact PII before cloud calls".to_string(),
        condition: RuleCondition::Always,
        action: RuleAction::Redact {
            fields: vec!["ssn".to_string(), "salary".to_string()],
        },
    });

    store
}

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

    #[test]
    fn staffing_rules_registers_five_rules() {
        // 4 staffing.fill rules + 1 staffing.any rule = 5 total.
        // Regression guard: if someone adds a rule to this module
        // without updating the count, this test surfaces it.
        let store = staffing_rules(TruthStore::new());
        let fill = store.get_rules("staffing.fill").len();
        let any = store.get_rules("staffing.any").len();
        assert_eq!(fill, 4);
        assert_eq!(any, 1);
    }

    #[test]
    fn blacklisted_worker_fails_the_rule() {
        let store = staffing_rules(TruthStore::new());
        let ctx = serde_json::json!({
            "worker": { "client_blacklisted": "true" }
        });
        let outcomes = store.evaluate("staffing.fill", &ctx);
        let blk = outcomes.iter().find(|o| o.rule_id == "client-not-blacklisted").unwrap();
        assert!(!blk.passed, "blacklisted worker must fail the rule");
    }

    #[test]
    fn missing_deadline_fires_reject_via_empty_condition() {
        let store = staffing_rules(TruthStore::new());
        // FieldEmpty passes when the field is missing — and the rule's
        // action is Reject, so enforcement should fire.
        let ctx = serde_json::json!({});
        let outcomes = store.evaluate("staffing.fill", &ctx);
        let deadline = outcomes.iter().find(|o| o.rule_id == "deadline-required").unwrap();
        assert!(deadline.passed);
        match &deadline.action {
            RuleAction::Reject { message } => assert!(message.contains("deadline")),
            _ => panic!("expected Reject action"),
        }
    }
}
