//! Core gatekeep behavior tests.

use gatekeep::{
    ClauseLabel, DenyShape, Effect, Fact, FactId, KnownFacts, Lattice, ObligationSpec,
    PartialFacts, Policy, Presence, Residual, ResidualPolicy, ResidualPolicyBranch,
    ResidualPolicyNode, StaticFactId, StaticObligationId, complete_residual, condition, evaluate,
    partial_evaluate, policy, required_facts,
};
use proptest::prelude::*;

#[derive(Debug, thiserror::Error)]
enum TestError {
    #[error(transparent)]
    Gatekeep(#[from] gatekeep::GatekeepError),
    #[error(transparent)]
    Trace(#[from] gatekeep::TraceError),
    #[error(transparent)]
    Postcard(#[from] postcard::Error),
    #[error("{0}")]
    Message(&'static str),
}

fn assert_denial_metadata(
    decision: &gatekeep::Decision<ReadTier>,
    expected_label: &str,
    expected_reason: Option<&str>,
    expected_shape: DenyShape,
) -> Result<(), TestError> {
    let gatekeep::DecisiveClause::Deny {
        label,
        reason,
        shape,
        ..
    } = &decision.trace.decisive
    else {
        return Err(TestError::Message("decision should deny"));
    };
    let Some(label) = label else {
        return Err(TestError::Message("denial should carry label"));
    };
    assert_eq!(label.as_str(), expected_label);
    assert_eq!(
        reason.as_ref().map(gatekeep::ReasonCode::as_str),
        expected_reason
    );
    assert_eq!(*shape, expected_shape);
    Ok(())
}

fn assert_denial_fact_params(
    decision: &gatekeep::Decision<ReadTier>,
    expected: &[&str],
) -> Result<(), TestError> {
    let Some(reason) = decision.denial_reason()? else {
        return Err(TestError::Message("decision should have denial reason"));
    };
    let facts = reason
        .params
        .values()
        .filter_map(|value| match value {
            gatekeep::ReasonValue::Fact(fact) => Some(fact.as_str()),
            _ => None,
        })
        .collect::<Vec<_>>();
    assert_eq!(facts, expected);
    Ok(())
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
enum ReadTier {
    Released,
    Shared,
    Full,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
enum Scope {
    Left,
    Right,
    Both,
    None,
}

impl Lattice for Scope {
    fn meet(&self, other: &Self) -> Self {
        match (*self, *other) {
            (Self::None, _)
            | (_, Self::None)
            | (Self::Left, Self::Right)
            | (Self::Right, Self::Left) => Self::None,
            (Self::Both, value) | (value, Self::Both) => value,
            (Self::Left, Self::Left) => Self::Left,
            (Self::Right, Self::Right) => Self::Right,
        }
    }

    fn join(&self, other: &Self) -> Self {
        match (*self, *other) {
            (Self::Both, _)
            | (_, Self::Both)
            | (Self::Left, Self::Right)
            | (Self::Right, Self::Left) => Self::Both,
            (Self::None, value) | (value, Self::None) => value,
            (Self::Left, Self::Left) => Self::Left,
            (Self::Right, Self::Right) => Self::Right,
        }
    }

    fn top() -> Self {
        Self::Both
    }

    fn bottom() -> Self {
        Self::None
    }
}

impl Lattice for ReadTier {
    fn meet(&self, other: &Self) -> Self {
        std::cmp::min(*self, *other)
    }

    fn join(&self, other: &Self) -> Self {
        std::cmp::max(*self, *other)
    }

    fn top() -> Self {
        Self::Full
    }

    fn bottom() -> Self {
        Self::Released
    }
}

struct RoleAccess;
impl Fact for RoleAccess {
    const ID: StaticFactId = StaticFactId::new("role_access");
}

struct ResourceShared;
impl Fact for ResourceShared {
    const ID: StaticFactId = StaticFactId::new("resource_shared");
}

struct ResourceFull;
impl Fact for ResourceFull {
    const ID: StaticFactId = StaticFactId::new("resource_full");
}

struct Suspended;
impl Fact for Suspended {
    const ID: StaticFactId = StaticFactId::new("suspended");
}

struct BreakGlass;
impl ObligationSpec for BreakGlass {
    const ID: StaticObligationId = StaticObligationId::new("break_glass");
}

struct AuditOverride;
impl ObligationSpec for AuditOverride {
    const ID: StaticObligationId = StaticObligationId::new("audit_override");
}

#[test]
fn empty_policy_combinators_fail_closed() {
    let facts = KnownFacts::new();

    assert_eq!(
        evaluate(&policy::all::<ReadTier>([]), &facts).effect,
        Effect::Deny
    );
    assert_eq!(
        evaluate(&policy::any::<ReadTier>([]), &facts).effect,
        Effect::Deny
    );
}

#[test]
fn grant_denial_carries_reason_metadata() -> Result<(), TestError> {
    let decision = evaluate(
        &policy::grant(ReadTier::Full, condition::has::<RoleAccess>())
            .try_labeled("case_read")?
            .try_reason("case_read_denied")?
            .hidden(),
        &KnownFacts::new(),
    );

    let Some(reason) = decision.denial_reason()? else {
        return Err(TestError::Message(
            "denied grant should have reason metadata",
        ));
    };
    assert_eq!(reason.code.as_str(), "case_read_denied");
    assert_eq!(reason.shape, DenyShape::Hidden);
    assert!(
        reason
            .params
            .keys()
            .any(|key| key.as_str() == "missing_fact")
    );
    Ok(())
}

#[test]
fn deny_when_denies_with_configured_reason() -> Result<(), TestError> {
    let decision = evaluate(
        &policy::deny_when::<ReadTier>(
            condition::has::<Suspended>(),
            gatekeep::ReasonCode::new("suspended")?,
        ),
        &KnownFacts::new().with_present::<Suspended>(),
    );

    assert_eq!(decision.effect, Effect::Deny);
    let Some(reason) = decision.denial_reason()? else {
        return Err(TestError::Message("deny_when should expose reason"));
    };
    assert_eq!(reason.code.as_str(), "suspended");
    Ok(())
}

#[test]
fn all_preserves_ordered_deny_precedence() -> Result<(), TestError> {
    let policy = policy::all([
        policy::deny_when::<ReadTier>(
            condition::has::<Suspended>(),
            gatekeep::ReasonCode::new("suspended")?,
        ),
        policy::deny_when::<ReadTier>(
            condition::has::<ResourceShared>(),
            gatekeep::ReasonCode::new("shared")?,
        ),
        policy::grant(ReadTier::Full, condition::always()),
    ]);
    let facts = KnownFacts::new()
        .with_present::<Suspended>()
        .with_present::<ResourceShared>();

    let decision = evaluate(&policy, &facts);

    let Some(reason) = decision.denial_reason()? else {
        return Err(TestError::Message("decision should deny"));
    };
    assert_eq!(reason.code.as_str(), "suspended");
    Ok(())
}

#[test]
fn all_denial_only_reports_the_failing_fact() -> Result<(), TestError> {
    let policy = policy::grant(
        ReadTier::Full,
        condition::all([
            condition::has::<RoleAccess>(),
            condition::has::<ResourceFull>(),
        ]),
    )
    .try_labeled("case_read")?;

    let decision = evaluate(&policy, &KnownFacts::new().with_present::<RoleAccess>());
    let Some(reason) = decision.denial_reason()? else {
        return Err(TestError::Message(
            "denied grant should have reason metadata",
        ));
    };

    let missing = reason
        .params
        .values()
        .filter_map(|value| match value {
            gatekeep::ReasonValue::Fact(fact) => Some(fact.as_str()),
            _ => None,
        })
        .collect::<Vec<_>>();
    assert_eq!(missing, vec!["resource_full"]);
    Ok(())
}

#[test]
fn all_uses_meet_and_obligations_from_meet_arm() {
    let policy = policy::all([
        policy::grant(ReadTier::Full, condition::always()).with_obligation::<BreakGlass>(),
        policy::grant(ReadTier::Shared, condition::always()).with_obligation::<AuditOverride>(),
    ]);

    let decision = evaluate(&policy, &KnownFacts::new());

    assert_eq!(decision.effect, Effect::Permit(ReadTier::Shared));
    assert_eq!(decision.obligations.len(), 1);
    assert_eq!(decision.obligations[0].as_str(), "audit_override");
}

#[test]
fn any_supports_non_total_lattice_join() -> Result<(), TestError> {
    let policy = policy::any([
        policy::grant(Scope::Left, condition::always()),
        policy::grant(Scope::Right, condition::always()),
    ]);

    let decision = evaluate(&policy, &KnownFacts::new());

    assert_eq!(decision.effect, Effect::Permit(Scope::Both));
    match decision.trace.decisive {
        gatekeep::DecisiveClause::Permit { granted, .. } => {
            assert_eq!(granted, Scope::Both);
        }
        gatekeep::DecisiveClause::Deny { .. } => {
            return Err(TestError::Message("non-total join test should permit"));
        }
    }
    Ok(())
}

#[test]
fn all_synthesized_meet_trace_matches_effect() -> Result<(), TestError> {
    let policy = policy::all([
        policy::grant(Scope::Left, condition::always()),
        policy::grant(Scope::Right, condition::always()),
    ]);

    let decision = evaluate(&policy, &KnownFacts::new());

    assert_eq!(decision.effect, Effect::Permit(Scope::None));
    match decision.trace.decisive {
        gatekeep::DecisiveClause::Permit { granted, .. } => {
            assert_eq!(granted, Scope::None);
        }
        gatekeep::DecisiveClause::Deny { .. } => {
            return Err(TestError::Message("non-total meet test should permit"));
        }
    }
    Ok(())
}

#[test]
fn any_unions_obligations_for_arms_at_winning_grade() {
    let policy = policy::any([
        policy::grant(ReadTier::Full, condition::always()).with_obligation::<BreakGlass>(),
        policy::grant(ReadTier::Full, condition::always()).with_obligation::<AuditOverride>(),
        policy::grant(ReadTier::Shared, condition::always()),
    ]);

    let decision = evaluate(&policy, &KnownFacts::new());

    assert_eq!(decision.effect, Effect::Permit(ReadTier::Full));
    assert_eq!(decision.obligations.len(), 2);
    assert_eq!(decision.obligations[0].as_str(), "break_glass");
    assert_eq!(decision.obligations[1].as_str(), "audit_override");
}

#[test]
fn or_else_skips_fallback_when_primary_permits() {
    let policy = policy::or_else(
        policy::grant(ReadTier::Shared, condition::has::<RoleAccess>()),
        policy::grant(ReadTier::Full, condition::has::<ResourceFull>())
            .with_obligation::<BreakGlass>(),
    );
    let facts = KnownFacts::new().with_present::<RoleAccess>();

    let decision = evaluate(&policy, &facts);

    assert_eq!(decision.effect, Effect::Permit(ReadTier::Shared));
    assert!(decision.obligations.is_empty());
    assert_eq!(decision.trace.consulted.len(), 1);
    assert_eq!(decision.trace.consulted[0].0.as_str(), "role_access");
}

#[test]
fn partial_any_keeps_top_permit_pending_to_preserve_obligations() -> Result<(), TestError> {
    let policy = policy::any([
        policy::grant(ReadTier::Full, condition::has::<RoleAccess>())
            .with_obligation::<BreakGlass>(),
        policy::grant(ReadTier::Full, condition::has::<ResourceFull>())
            .with_obligation::<AuditOverride>(),
    ]);
    let partial = PartialFacts::new()
        .with_present::<RoleAccess>()
        .with_unknown::<ResourceFull>();
    let completed = KnownFacts::new()
        .with_present::<RoleAccess>()
        .with_present::<ResourceFull>();

    let reduced = partial_evaluate(&policy, &partial);
    let decision = match &reduced {
        Residual::Pending { .. } => complete_residual(&reduced, &completed),
        Residual::Resolved(_) => {
            return Err(TestError::Message(
                "top permit with pending top arm must remain pending",
            ));
        }
    };

    assert_eq!(decision.effect, Effect::Permit(ReadTier::Full));
    assert_eq!(decision.obligations.len(), 2);
    assert_eq!(decision.obligations[0].as_str(), "break_glass");
    assert_eq!(decision.obligations[1].as_str(), "audit_override");
    Ok(())
}

#[test]
fn partial_resolved_decision_keeps_consulted_facts_and_label() -> Result<(), TestError> {
    let policy = policy::any([
        policy::grant(ReadTier::Shared, condition::has::<RoleAccess>())
            .try_labeled("role_access")?,
        policy::grant(ReadTier::Released, condition::has::<ResourceShared>()),
    ]);
    let partial = PartialFacts::new()
        .with_present::<RoleAccess>()
        .with_absent::<ResourceShared>();

    let Residual::Resolved(decision) = partial_evaluate(&policy, &partial) else {
        return Err(TestError::Message("known facts should resolve policy"));
    };

    assert_eq!(decision.effect, Effect::Permit(ReadTier::Shared));
    assert_eq!(decision.trace.consulted.len(), 2);
    match decision.trace.decisive {
        gatekeep::DecisiveClause::Permit { label, .. } => {
            let Some(label) = label else {
                return Err(TestError::Message("permit trace should keep label"));
            };
            assert_eq!(label.as_str(), "role_access");
        }
        gatekeep::DecisiveClause::Deny { .. } => {
            return Err(TestError::Message("decision should permit"));
        }
    }
    Ok(())
}

#[test]
fn partial_any_all_deny_keeps_first_denial_label() -> Result<(), TestError> {
    let policy = policy::any([
        policy::grant(ReadTier::Full, condition::has::<RoleAccess>()).try_labeled("first")?,
        policy::grant(ReadTier::Full, condition::has::<ResourceFull>()).try_labeled("second")?,
    ]);
    let partial = PartialFacts::new()
        .with_absent::<RoleAccess>()
        .with_absent::<ResourceFull>();

    let Residual::Resolved(decision) = partial_evaluate(&policy, &partial) else {
        return Err(TestError::Message("all-deny partial any should resolve"));
    };

    match decision.trace.decisive {
        gatekeep::DecisiveClause::Deny { label, .. } => {
            let Some(label) = label else {
                return Err(TestError::Message("first denial label should be preserved"));
            };
            assert_eq!(label.as_str(), "first");
        }
        gatekeep::DecisiveClause::Permit { .. } => {
            return Err(TestError::Message("all-deny policy should deny"));
        }
    }
    Ok(())
}

#[test]
fn partial_any_pending_keeps_first_resolved_denial_metadata() -> Result<(), TestError> {
    let policy = policy::any([
        policy::grant(ReadTier::Full, condition::has::<RoleAccess>())
            .try_labeled("first")?
            .try_reason("first_denied")?
            .hidden(),
        policy::grant(ReadTier::Full, condition::has::<ResourceFull>()).try_labeled("second")?,
    ]);
    let partial = PartialFacts::new()
        .with_absent::<RoleAccess>()
        .with_unknown::<ResourceFull>();
    let completed = KnownFacts::new()
        .with_absent::<RoleAccess>()
        .with_absent::<ResourceFull>();

    let original = evaluate(&policy, &completed);
    let reduced = partial_evaluate(&policy, &partial);
    if !matches!(&reduced, Residual::Pending { .. }) {
        return Err(TestError::Message("unknown fact should keep any pending"));
    }
    let reduced = complete_residual(&reduced, &completed);

    assert_denial_metadata(&original, "first", Some("first_denied"), DenyShape::Hidden)?;
    assert_denial_metadata(&reduced, "first", Some("first_denied"), DenyShape::Hidden)?;
    assert_denial_fact_params(&original, &["role_access"])?;
    assert_denial_fact_params(&reduced, &["role_access"])?;
    Ok(())
}

#[test]
fn partial_or_else_pending_primary_keeps_fallback_denial_metadata() -> Result<(), TestError> {
    let policy = policy::or_else(
        policy::grant(ReadTier::Full, condition::has::<RoleAccess>()).try_labeled("primary")?,
        policy::grant(ReadTier::Shared, condition::has::<ResourceShared>())
            .try_labeled("fallback")?
            .try_reason("fallback_denied")?
            .hidden(),
    );
    let partial = PartialFacts::new()
        .with_unknown::<RoleAccess>()
        .with_absent::<ResourceShared>();
    let completed = KnownFacts::new()
        .with_absent::<RoleAccess>()
        .with_absent::<ResourceShared>();

    let original = evaluate(&policy, &completed);
    let reduced = partial_evaluate(&policy, &partial);
    if !matches!(&reduced, Residual::Pending { .. }) {
        return Err(TestError::Message(
            "unknown primary should keep or_else pending",
        ));
    }
    let reduced = complete_residual(&reduced, &completed);

    assert_denial_metadata(
        &original,
        "fallback",
        Some("fallback_denied"),
        DenyShape::Hidden,
    )?;
    assert_denial_metadata(
        &reduced,
        "fallback",
        Some("fallback_denied"),
        DenyShape::Hidden,
    )?;
    assert_denial_fact_params(&original, &["resource_shared"])?;
    assert_denial_fact_params(&reduced, &["resource_shared"])?;
    Ok(())
}

#[test]
fn complete_residual_merges_prior_and_residual_consulted_facts() {
    let policy = policy::all([
        policy::grant(ReadTier::Shared, condition::has::<RoleAccess>()),
        policy::grant(ReadTier::Full, condition::has::<ResourceFull>()),
    ]);
    let partial = PartialFacts::new()
        .with_present::<RoleAccess>()
        .with_unknown::<ResourceFull>();
    let completed = KnownFacts::new()
        .with_present::<RoleAccess>()
        .with_present::<ResourceFull>();
    let reduced = partial_evaluate(&policy, &partial);

    let decision = complete_residual(&reduced, &completed);
    let consulted = decision
        .trace
        .consulted
        .iter()
        .map(|(fact, _presence)| fact.as_str())
        .collect::<Vec<_>>();

    assert_eq!(consulted, vec!["role_access", "resource_full"]);
}

#[test]
fn required_facts_are_sorted_and_deduped() {
    let policy = policy::any([
        policy::grant(ReadTier::Full, condition::has::<ResourceFull>()),
        policy::grant(ReadTier::Shared, condition::has::<RoleAccess>()),
        policy::grant(ReadTier::Released, condition::has::<ResourceFull>()),
    ]);

    let facts = required_facts(&policy)
        .into_iter()
        .map(|fact| fact.as_str().to_owned())
        .collect::<Vec<_>>();

    assert_eq!(facts, vec!["resource_full", "role_access"]);
}

#[test]
fn residual_policy_introspection_detects_constants_and_obligations() -> Result<(), TestError> {
    let permit = ResidualPolicy::Permit(ReadTier::Shared);
    let deny = ResidualPolicy::<ReadTier>::Deny;
    let obligated_grant = ResidualPolicy::Grant {
        outcome: ReadTier::Full,
        condition: condition::has::<RoleAccess>(),
        label: None,
        deny_shape: DenyShape::Forbidden,
        obligations: vec![gatekeep::ObligationId::new("break_glass")?],
        reason: None,
    };
    let nested = ResidualPolicy::OrElse {
        primary: Box::new(deny.clone()),
        fallback: Box::new(obligated_grant.clone()),
    };

    assert!(permit.is_permit_constant());
    assert!(!permit.is_deny_constant());
    assert!(deny.is_deny_constant());
    assert!(deny.is_constant());
    assert!(!permit.carries_obligation());
    assert!(obligated_grant.carries_obligation());
    assert!(nested.carries_obligation());
    Ok(())
}

#[test]
fn residual_policy_fold_visits_bottom_up_in_source_order() {
    let residual = ResidualPolicy::OrElse {
        primary: Box::new(ResidualPolicy::All(vec![
            ResidualPolicy::Permit(ReadTier::Shared),
            ResidualPolicy::Deny,
        ])),
        fallback: Box::new(ResidualPolicy::Permit(ReadTier::Full)),
    };

    let folded = residual.fold(&mut |node| match node {
        ResidualPolicyNode::Permit(outcome) => format!("permit:{outcome:?}"),
        ResidualPolicyNode::Deny => "deny".to_owned(),
        ResidualPolicyNode::PermitWithTrace { .. } => "permit_trace".to_owned(),
        ResidualPolicyNode::DenyWithTrace { .. } => "deny_trace".to_owned(),
        ResidualPolicyNode::Grant { .. } => "grant".to_owned(),
        ResidualPolicyNode::All { arms, .. } => format!("all({})", arms.join(",")),
        ResidualPolicyNode::Any { arms, .. } => format!("any({})", arms.join(",")),
        ResidualPolicyNode::OrElse {
            primary, fallback, ..
        } => {
            let fallback = fallback.unwrap_or_else(|| "skipped".to_owned());
            format!("or_else({primary},{fallback})")
        }
    });

    assert_eq!(folded, "or_else(all(permit:Shared,deny),permit:Full)");
}

#[test]
fn residual_policy_try_fold_pruned_skips_selected_fallback() -> Result<(), TestError> {
    let residual = ResidualPolicy::OrElse {
        primary: Box::new(ResidualPolicy::Permit(ReadTier::Shared)),
        fallback: Box::new(ResidualPolicy::Grant {
            outcome: ReadTier::Full,
            condition: condition::has::<RoleAccess>(),
            label: None,
            deny_shape: DenyShape::Forbidden,
            obligations: vec![gatekeep::ObligationId::new("break_glass")?],
            reason: None,
        }),
    };

    let folded = residual.try_fold_pruned(
        &mut |branch| match branch {
            ResidualPolicyBranch::OrElseFallback { fallback, .. } => !fallback.carries_obligation(),
        },
        &mut |node| -> Result<_, TestError> {
            Ok(match node {
                ResidualPolicyNode::Permit(outcome) => format!("permit:{outcome:?}"),
                ResidualPolicyNode::Deny => "deny".to_owned(),
                ResidualPolicyNode::PermitWithTrace { .. } => "permit_trace".to_owned(),
                ResidualPolicyNode::DenyWithTrace { .. } => "deny_trace".to_owned(),
                ResidualPolicyNode::Grant { .. } => "grant".to_owned(),
                ResidualPolicyNode::All { arms, .. } => format!("all({})", arms.join(",")),
                ResidualPolicyNode::Any { arms, .. } => format!("any({})", arms.join(",")),
                ResidualPolicyNode::OrElse {
                    primary, fallback, ..
                } => {
                    let fallback = fallback.unwrap_or_else(|| "skipped".to_owned());
                    format!("or_else({primary},{fallback})")
                }
            })
        },
    )?;

    assert_eq!(folded, "or_else(permit:Shared,skipped)");
    Ok(())
}

#[test]
fn policy_hash_changes_with_structure() -> Result<(), TestError> {
    let first = policy::grant(ReadTier::Full, condition::has::<RoleAccess>());
    let second = policy::grant(ReadTier::Full, condition::always());

    assert_ne!(first.hash()?, second.hash()?);
    Ok(())
}

#[test]
fn known_facts_reject_unknown_presence() -> Result<(), TestError> {
    let result = KnownFacts::from_entries([(FactId::new("resource_full")?, Presence::Unknown)]);

    assert!(result.is_err());
    Ok(())
}

#[test]
fn known_facts_deserialization_rejects_unknown_presence() {
    let value = serde_json::json!({
        "role_access": ["Unknown", null]
    });

    let result = serde_json::from_value::<KnownFacts>(value);

    assert!(result.is_err());
}

proptest! {
    #[test]
    fn partial_evaluation_preserves_effect_and_obligations(
        role_present in any::<bool>(),
        shared_present in any::<bool>(),
        full_present in any::<bool>(),
    ) {
        let policy = list_policy().map_err(|error| TestCaseError::fail(error.to_string()))?;
        let role_access = FactId::new("role_access")
            .map_err(|error| TestCaseError::fail(error.to_string()))?;
        let resource_shared = FactId::new("resource_shared")
            .map_err(|error| TestCaseError::fail(error.to_string()))?;
        let resource_full = FactId::new("resource_full")
            .map_err(|error| TestCaseError::fail(error.to_string()))?;
        let partial = PartialFacts::new()
            .with_fact(
                role_access.clone(),
                if role_present { Presence::Present } else { Presence::Absent },
            )
            .with_unknown::<ResourceShared>()
            .with_unknown::<ResourceFull>();
        let completed = KnownFacts::from_entries([
            (
                role_access,
                if role_present { Presence::Present } else { Presence::Absent },
            ),
            (
                resource_shared,
                if shared_present { Presence::Present } else { Presence::Absent },
            ),
            (
                resource_full,
                if full_present { Presence::Present } else { Presence::Absent },
            ),
        ])
        .map_err(|error| TestCaseError::fail(error.to_string()))?;

        let original = evaluate(&policy, &completed);
        let reduced = partial_evaluate(&policy, &partial);
        let residual = match reduced {
            Residual::Resolved(decision) => decision,
            Residual::Pending { .. } => complete_residual(&reduced, &completed),
        };

        prop_assert_eq!(residual.effect, original.effect);
        prop_assert_eq!(residual.obligations, original.obligations);
    }
}

fn list_policy() -> Result<Policy<ReadTier>, gatekeep::GatekeepError> {
    Ok(policy::or_else(
        policy::all([
            policy::grant(ReadTier::Full, condition::has::<RoleAccess>()),
            policy::any([
                policy::grant(ReadTier::Full, condition::has::<ResourceFull>()),
                policy::grant(ReadTier::Shared, condition::has::<ResourceShared>()),
            ]),
        ]),
        policy::grant(ReadTier::Full, condition::has::<ResourceFull>())
            .labeled(ClauseLabel::new("break_glass")?)
            .with_obligation::<BreakGlass>(),
    ))
}
