//! P4-04 — Policy pack versioning + downgrade rejection (admission gate).
//!
//! Verifies the post-review Lane P4 hardening (P4-04): a `PolicyPack` document
//! whose `spec.version` is **strictly lower** than the runtime's compiled-in
//! supported floor (`MIN_SUPPORTED_POLICY_PACK_VERSION`) is rejected.
//!
//! D11 boundary: `cellos-core` does not read process env vars. The strict-vs-
//! permissive choice is an explicit `allow_downgrade: bool` parameter on
//! [`check_policy_pack_version_compatibility`]; [`validate_policy_pack_document`]
//! is structural-only and always strict. The env-driven
//! `CELLOS_POLICY_ALLOW_DOWNGRADE=1` operator override lives on the supervisor
//! side (`crates/cellos-supervisor/src/composition.rs`) — that path exercises
//! the override end-to-end via the supervisor's policy-pack admission.
//!
//! See also: `crates/cellos-core/src/policy.rs` (the validator + free
//! function) and `crates/cellos-core/src/spec_validation.rs::check_policy_pack_version`
//! (façade).

use cellos_core::{
    check_policy_pack_version_compatibility, validate_policy_pack_document, PolicyPackDocument,
    PolicyPackSpec, PolicyRules, MIN_SUPPORTED_POLICY_PACK_VERSION,
};

/// Build a minimal-but-valid `PolicyPackDocument` with the supplied
/// `spec.version`.
fn doc_with_version(version: Option<&str>) -> PolicyPackDocument {
    PolicyPackDocument {
        api_version: "cellos.io/v1".into(),
        kind: "PolicyPack".into(),
        spec: PolicyPackSpec {
            id: "p4-04-test-pack".into(),
            description: None,
            version: version.map(|s| s.to_string()),
            placement: None,
            rules: PolicyRules::default(),
        },
    }
}

/// Construct a triple strictly lower than `MIN_SUPPORTED_POLICY_PACK_VERSION`.
///
/// Hand-coded against the current floor (`"1.0.0"`). If a future bump moves the
/// floor, this constant must move with it — the test would otherwise silently
/// degrade to "no downgrade scenario was actually exercised".
const TOO_OLD_VERSION: &str = "0.9.0";

#[test]
fn p4_04_floor_constant_matches_test_assumption() {
    // Pin the floor the rest of this file is calibrated against. If the floor
    // is bumped (entirely reasonable), this assertion is the canary that
    // forces a test-side update.
    assert_eq!(
        MIN_SUPPORTED_POLICY_PACK_VERSION, "1.0.0",
        "P4-04 test fixtures (TOO_OLD_VERSION = 0.9.0) assume a 1.0.0 floor; \
         update the test if the runtime floor is bumped"
    );
}

/// Pure-function coverage of the version gate's strict-vs-permissive arms.
///
/// `cellos-core` is env-free (D11), so the contract here is parameterised on
/// the explicit `allow_downgrade: bool`. The env-driven
/// `CELLOS_POLICY_ALLOW_DOWNGRADE=1` operator override lives on the
/// supervisor side; that integration is exercised in
/// `crates/cellos-supervisor/tests/` once the supervisor admission path is
/// directly testable from this surface.
///
/// Cases covered:
///   1. allow_downgrade=false + too-old version → rejected; rejection message
///      mentions the floor and the operator-override env var name.
///   2. allow_downgrade=true + too-old version → admitted.
///   3. allow_downgrade=true + structurally malformed semver → STILL rejected
///      (the override suppresses downgrade rejections only, not structural
///      errors).
///   4. validate_policy_pack_document is strict-by-construction (always calls
///      the gate with allow_downgrade=false).
#[test]
fn p4_04_version_gate_arms() {
    // ── Case 1: strict mode rejects too-old version ─────────────────────────
    let err = check_policy_pack_version_compatibility(Some(TOO_OLD_VERSION), false)
        .expect_err("strict mode must reject too-old version");
    let msg = err.to_string();
    assert!(
        msg.contains("older than runtime-supported floor"),
        "rejection message must explain the version floor; got {msg:?}"
    );
    assert!(
        msg.contains("CELLOS_POLICY_ALLOW_DOWNGRADE"),
        "rejection message must mention the operator override env var; got {msg:?}"
    );

    // ── Case 2: permissive mode admits the same version ─────────────────────
    assert!(
        check_policy_pack_version_compatibility(Some(TOO_OLD_VERSION), true).is_ok(),
        "allow_downgrade=true must admit a too-old version"
    );

    // ── Case 3: malformed version still rejected even when allow_downgrade ──
    assert!(
        check_policy_pack_version_compatibility(Some("not-a-semver"), true).is_err(),
        "structural errors must surface regardless of the downgrade override"
    );

    // ── Case 4: validate_policy_pack_document is strict by construction ─────
    let doc_old = doc_with_version(Some(TOO_OLD_VERSION));
    assert!(
        validate_policy_pack_document(&doc_old).is_err(),
        "validate_policy_pack_document must always reject downgrades — env override lives on the supervisor side"
    );
}

#[test]
fn p4_04_admission_accepts_floor_and_above_versions() {
    // These cases never consult `CELLOS_POLICY_ALLOW_DOWNGRADE` — they pass
    // before the override branch is reached — so they are safe to run in
    // parallel with `p4_04_version_admission_phases`.
    assert!(validate_policy_pack_document(&doc_with_version(None)).is_ok());
    assert!(validate_policy_pack_document(&doc_with_version(Some(
        MIN_SUPPORTED_POLICY_PACK_VERSION
    )))
    .is_ok());
    assert!(validate_policy_pack_document(&doc_with_version(Some("1.5.7"))).is_ok());
    assert!(validate_policy_pack_document(&doc_with_version(Some("2.0.0"))).is_ok());
    assert!(validate_policy_pack_document(&doc_with_version(Some("1.0.0-rc.1"))).is_ok());
}
