//! Authority contracts — named, reusable runtime authority definitions.
//!
//! Contracts are the next-step policy surface for `tsafe exec`: instead of
//! spelling policy as a bundle of flags, a repo manifest can declare a named
//! contract that describes:
//!
//! - which vault profile / namespace to resolve from
//! - which secret names are allowed and required
//! - which targets may receive injected authority
//! - which trust posture (`standard`, `hardened`, or explicit `custom`) applies
//! - future intent such as network policy
//!
//! This module is core-only. It parses and validates contract manifests so a
//! higher layer (CLI, audit UI, future policy engine) can execute against a
//! stable, auditable model.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::baseline_contracts::{
    ci_deploy_contract, ops_emergency_contract, read_only_contract, AccessLevel,
};
use crate::deny_reason::DenyReason;
use crate::errors::{SafeError, SafeResult};
use crate::namespace_bulk::validate_namespace_segment;
use crate::profile::validate_profile_name;
use crate::pullconfig::find_config;
use crate::rbac::RbacProfile;
use crate::vault::validate_secret_key;

/// A validated named authority contract.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthorityContract {
    pub name: String,
    pub profile: Option<String>,
    pub namespace: Option<String>,
    pub access_profile: RbacProfile,
    pub allow_all_secrets: bool,
    pub allowed_secrets: Vec<String>,
    pub required_secrets: Vec<String>,
    pub allowed_targets: Vec<String>,
    pub trust: AuthorityTrust,
    pub network: AuthorityNetworkPolicy,
}

impl AuthorityContract {
    /// Resolve the trust posture into a concrete exec policy.
    pub fn resolved_exec_policy(&self) -> ResolvedAuthorityPolicy {
        match &self.trust {
            AuthorityTrust::Standard => ResolvedAuthorityPolicy {
                trust_level: AuthorityTrustLevel::Standard,
                access_profile: self.access_profile,
                inherit: AuthorityInheritMode::Full,
                deny_dangerous_env: false,
                redact_output: false,
            },
            AuthorityTrust::Hardened => ResolvedAuthorityPolicy {
                trust_level: AuthorityTrustLevel::Hardened,
                access_profile: self.access_profile,
                inherit: AuthorityInheritMode::Minimal,
                deny_dangerous_env: true,
                redact_output: true,
            },
            AuthorityTrust::Custom(custom) => ResolvedAuthorityPolicy {
                trust_level: AuthorityTrustLevel::Custom,
                access_profile: self.access_profile,
                inherit: custom.inherit,
                deny_dangerous_env: custom.deny_dangerous_env,
                redact_output: custom.redact_output,
            },
        }
    }

    /// Return whether this contract requires no secret names.
    pub fn requires_zero_secrets(&self) -> bool {
        self.required_secrets.is_empty()
    }

    /// Return whether this contract's secret policy permits no injection.
    ///
    /// An omitted `allowed_secrets` field compiles to `allow_all_secrets`, so
    /// it is not the same as an explicit empty allowlist.
    pub fn injects_zero_secrets(&self) -> bool {
        !self.allow_all_secrets && self.allowed_secrets.is_empty()
    }

    /// Return whether this contract has the conservative no-secret diagnostic
    /// shape used until the schema grows an explicit diagnostic marker.
    pub fn is_no_secret_diagnostic_contract(&self) -> bool {
        self.requires_zero_secrets() && self.injects_zero_secrets()
    }

    /// Return whether this contract allows the given execution target.
    ///
    /// Targets are matched exactly against the provided string and, when that
    /// string is a path, also against the basename. An empty target allowlist
    /// means "no target restriction yet".
    pub fn allows_target(&self, command: &str) -> bool {
        self.evaluate_target(Some(command)).decision.is_allowed()
    }

    /// Evaluate a target command against this contract's allowlist.
    ///
    /// The result is explicit and auditable: higher layers can tell whether a
    /// command was allowed by exact match, basename match, lack of restriction,
    /// or denied outright.
    pub fn evaluate_target(&self, command: Option<&str>) -> AuthorityTargetEvaluation {
        if self.allowed_targets.is_empty() {
            return AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::Unconstrained,
                matched_allowlist_entry: None,
            };
        }

        let Some(command) = command.map(str::trim).filter(|value| !value.is_empty()) else {
            return AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::MissingTarget,
                matched_allowlist_entry: None,
            };
        };

        if let Some(matched) = self
            .allowed_targets
            .iter()
            .find(|allowed| allowed == &command)
        {
            return AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::AllowedExact,
                matched_allowlist_entry: Some(matched.clone()),
            };
        }

        let basename = Path::new(command)
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or(command);
        if let Some(matched) = self
            .allowed_targets
            .iter()
            .find(|allowed| allowed.as_str() == basename)
        {
            return AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::AllowedBasename,
                matched_allowlist_entry: Some(matched.clone()),
            };
        }

        AuthorityTargetEvaluation {
            decision: AuthorityTargetDecision::Denied,
            matched_allowlist_entry: None,
        }
    }
}

/// Bound firewall target policy validation errors.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundFirewallTargetPolicyError {
    EmptyTargetAllowlist,
}

impl BoundFirewallTargetPolicyError {
    /// Return the stable denial reason that represents this policy error.
    pub fn deny_reason(self) -> DenyReason {
        match self {
            Self::EmptyTargetAllowlist => DenyReason::BlankScope,
        }
    }
}

/// Validate target policy for bound MCP firewall mode.
///
/// Standard contract evaluation still treats an empty target allowlist as
/// unconstrained for legacy CLI compatibility. Bound MCP never does: it is the
/// model-facing firewall surface, so every bound contract needs an explicit
/// target policy before execution can proceed.
pub fn validate_bound_firewall_target_policy(
    contract: &AuthorityContract,
) -> Result<(), BoundFirewallTargetPolicyError> {
    if contract.allowed_targets.is_empty() {
        return Err(BoundFirewallTargetPolicyError::EmptyTargetAllowlist);
    }
    Ok(())
}

/// Validate bound firewall network policy against host enforcement capability.
pub fn validate_bound_firewall_network_policy(
    contract: &AuthorityContract,
    network_restriction_enforceable: bool,
) -> Result<(), DenyReason> {
    if matches!(contract.network, AuthorityNetworkPolicy::Restricted)
        && !network_restriction_enforceable
    {
        return Err(DenyReason::NetworkUnenforced);
    }
    Ok(())
}

/// High-level trust posture for a contract.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityTrustLevel {
    Standard,
    Hardened,
    Custom,
}

impl AuthorityTrustLevel {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Standard => "standard",
            Self::Hardened => "hardened",
            Self::Custom => "custom",
        }
    }
}

/// Resolved trust model for a contract.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorityTrust {
    Standard,
    Hardened,
    Custom(CustomAuthorityTrust),
}

/// Explicit trust overrides for `trust_level: custom`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomAuthorityTrust {
    pub inherit: AuthorityInheritMode,
    pub deny_dangerous_env: bool,
    pub redact_output: bool,
}

/// Parent-environment inheritance mode for exec authority.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityInheritMode {
    Full,
    Minimal,
    Clean,
}

impl AuthorityInheritMode {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Full => "full",
            Self::Minimal => "minimal",
            Self::Clean => "clean",
        }
    }
}

/// Future network-intent policy for a contract.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityNetworkPolicy {
    #[default]
    Inherit,
    Restricted,
}

impl AuthorityNetworkPolicy {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Inherit => "inherit",
            Self::Restricted => "restricted",
        }
    }
}

/// Concrete exec policy derived from a contract trust posture.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedAuthorityPolicy {
    pub trust_level: AuthorityTrustLevel,
    pub access_profile: RbacProfile,
    pub inherit: AuthorityInheritMode,
    pub deny_dangerous_env: bool,
    pub redact_output: bool,
}

/// Explicit result of checking a command against a contract allowlist.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityTargetDecision {
    Unconstrained,
    AllowedExact,
    AllowedBasename,
    MissingTarget,
    Denied,
}

impl AuthorityTargetDecision {
    pub fn is_allowed(self) -> bool {
        matches!(
            self,
            Self::Unconstrained | Self::AllowedExact | Self::AllowedBasename
        )
    }

    /// Return the stable denial reason for target decisions that deny execution.
    pub fn deny_reason(self) -> Option<DenyReason> {
        match self {
            Self::Denied => Some(DenyReason::TargetNotAllowed),
            Self::MissingTarget => Some(DenyReason::TargetMissing),
            Self::Unconstrained | Self::AllowedExact | Self::AllowedBasename => None,
        }
    }
}

/// Outcome of one contract target evaluation, including the allowlist entry
/// that matched when the decision was positive.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorityTargetEvaluation {
    pub decision: AuthorityTargetDecision,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matched_allowlist_entry: Option<String>,
}

#[derive(Debug, Default, Deserialize)]
struct RawContractsFile {
    #[serde(default)]
    contracts: BTreeMap<String, RawAuthorityContract>,
}

#[derive(Debug, Default, Deserialize)]
struct RawAuthorityContract {
    /// Optional built-in template to inherit defaults from.
    /// Available: "read_only", "ci_deploy", "ops_emergency".
    /// Explicit fields override template defaults.
    #[serde(default)]
    template: Option<String>,
    #[serde(default)]
    profile: Option<String>,
    #[serde(default)]
    namespace: Option<String>,
    #[serde(default)]
    access_profile: Option<RbacProfile>,
    #[serde(default)]
    allowed_secrets: Option<Vec<String>>,
    #[serde(default)]
    required_secrets: Vec<String>,
    #[serde(default)]
    allowed_targets: Vec<String>,
    #[serde(default)]
    trust_level: Option<AuthorityTrustLevel>,
    #[serde(default)]
    inherit: Option<AuthorityInheritMode>,
    #[serde(default)]
    deny_dangerous_env: Option<bool>,
    #[serde(default)]
    redact_output: Option<bool>,
    #[serde(default)]
    network: AuthorityNetworkPolicy,
}

/// Search upward from `start` for a repo manifest that may contain contracts.
pub fn find_contracts_manifest(start: &Path) -> Option<PathBuf> {
    find_config(start)
}

/// Load and validate all authority contracts from a repo manifest file.
///
/// The manifest may also contain other top-level sections (for example `pulls`);
/// this loader ignores everything except `contracts`.
pub fn load_contracts(path: &Path) -> SafeResult<BTreeMap<String, AuthorityContract>> {
    let content = std::fs::read_to_string(path)?;
    let raw: RawContractsFile = if is_json(path) {
        serde_json::from_str(&content).map_err(|e| SafeError::InvalidVault {
            reason: format!("invalid authority contract JSON: {e}"),
        })?
    } else {
        serde_yaml::from_str(&content).map_err(|e| SafeError::InvalidVault {
            reason: format!("invalid authority contract YAML: {e}"),
        })?
    };

    raw.contracts
        .into_iter()
        .map(|(name, raw)| Ok((name.clone(), validate_contract(name, raw)?)))
        .collect()
}

/// Load a single named authority contract from a manifest file.
pub fn load_contract(path: &Path, name: &str) -> SafeResult<AuthorityContract> {
    let contracts = load_contracts(path)?;
    contracts
        .get(name)
        .cloned()
        .ok_or_else(|| SafeError::InvalidVault {
            reason: format!(
                "authority contract '{name}' not found in {}",
                path.display()
            ),
        })
}

fn apply_template_defaults(contract_name: &str, raw: &mut RawAuthorityContract) -> SafeResult<()> {
    let tmpl_name = match raw.template.as_deref() {
        Some(n) => n,
        None => return Ok(()),
    };
    let baseline = match tmpl_name {
        "read_only" => read_only_contract(),
        "ci_deploy" => ci_deploy_contract(vec!["terraform".to_string(), "ansible".to_string()]),
        "ops_emergency" => ops_emergency_contract(),
        other => {
            return Err(SafeError::InvalidVault {
                reason: format!(
                    "contract '{contract_name}': unknown template '{other}' \
                     (available: read_only, ci_deploy, ops_emergency)"
                ),
            })
        }
    };
    // Fill in unset fields from the template. Explicit YAML fields win.
    if raw.trust_level.is_none() {
        raw.trust_level = Some(match baseline.required_trust_profile.as_str() {
            "hardened" => AuthorityTrustLevel::Hardened,
            _ => AuthorityTrustLevel::Standard,
        });
    }
    if raw.access_profile.is_none() {
        raw.access_profile = Some(match baseline.access_level {
            AccessLevel::ReadOnly => RbacProfile::ReadOnly,
            AccessLevel::ReadWrite => RbacProfile::ReadWrite,
        });
    }
    if raw.allowed_secrets.is_none() {
        raw.allowed_secrets = Some(baseline.secret_constraints.allowed_secrets);
    }
    if raw.required_secrets.is_empty() {
        raw.required_secrets = baseline.secret_constraints.required_secrets;
    }
    if raw.allowed_targets.is_empty() {
        if let Some(targets) = baseline.target_constraints {
            raw.allowed_targets = targets;
        }
    }
    Ok(())
}

fn validate_contract(name: String, mut raw: RawAuthorityContract) -> SafeResult<AuthorityContract> {
    validate_contract_name(&name)?;
    let allowed_secrets_was_specified = raw.allowed_secrets.is_some();
    apply_template_defaults(&name, &mut raw)?;

    let trust_level = raw.trust_level.ok_or_else(|| SafeError::InvalidVault {
        reason: format!("contract '{name}': trust_level is required (or use template: <name>)"),
    })?;
    if matches!(
        trust_level,
        AuthorityTrustLevel::Standard | AuthorityTrustLevel::Hardened
    ) {
        reject_custom_overrides(&name, &raw)?;
    }

    let profile = raw.profile.map(|profile| profile.trim().to_string());
    if let Some(profile) = profile.as_deref() {
        validate_profile_name(profile)?;
    }

    let namespace = raw.namespace.map(|namespace| namespace.trim().to_string());
    if let Some(namespace) = namespace.as_deref() {
        validate_namespace_segment(namespace)?;
    }

    let allowed_secrets = normalize_contract_secret_names(
        &name,
        "allowed_secrets",
        raw.allowed_secrets.unwrap_or_default(),
    )?;
    let allow_all_secrets = !allowed_secrets_was_specified && allowed_secrets.is_empty();
    let required_secrets =
        normalize_contract_secret_names(&name, "required_secrets", raw.required_secrets)?;
    for secret in &required_secrets {
        if !allow_all_secrets && !allowed_secrets.iter().any(|allowed| allowed == secret) {
            return Err(SafeError::InvalidVault {
                reason: format!(
                    "contract '{name}': required secret '{secret}' must also appear in allowed_secrets"
                ),
            });
        }
    }

    let trust = match trust_level {
        AuthorityTrustLevel::Standard => AuthorityTrust::Standard,
        AuthorityTrustLevel::Hardened => AuthorityTrust::Hardened,
        AuthorityTrustLevel::Custom => AuthorityTrust::Custom(CustomAuthorityTrust {
            inherit: raw.inherit.unwrap_or(AuthorityInheritMode::Full),
            deny_dangerous_env: raw.deny_dangerous_env.unwrap_or(false),
            redact_output: raw.redact_output.unwrap_or(false),
        }),
    };
    let allowed_targets = normalize_allowed_targets(&name, raw.allowed_targets)?;

    Ok(AuthorityContract {
        name,
        profile,
        namespace,
        access_profile: raw.access_profile.unwrap_or_default(),
        allow_all_secrets,
        allowed_secrets,
        required_secrets,
        allowed_targets,
        trust,
        network: raw.network,
    })
}

fn reject_custom_overrides(name: &str, raw: &RawAuthorityContract) -> SafeResult<()> {
    if raw.inherit.is_some() || raw.deny_dangerous_env.is_some() || raw.redact_output.is_some() {
        return Err(SafeError::InvalidVault {
            reason: format!(
                "contract '{name}': inherit / deny_dangerous_env / redact_output are only valid with trust_level: custom"
            ),
        });
    }
    Ok(())
}

fn normalize_contract_secret_names(
    contract_name: &str,
    field: &str,
    names: Vec<String>,
) -> SafeResult<Vec<String>> {
    let mut out = Vec::new();
    for name in names {
        let trimmed = name.trim();
        if trimmed.is_empty() {
            return Err(SafeError::InvalidVault {
                reason: format!("contract '{contract_name}': {field} contains an empty name"),
            });
        }
        if trimmed.contains('/') {
            return Err(SafeError::InvalidVault {
                reason: format!(
                    "contract '{contract_name}': {field} entry '{trimmed}' must be a post-namespace secret/env name, not a namespaced vault key"
                ),
            });
        }
        validate_secret_key(trimmed)?;
        if !out.iter().any(|existing: &String| existing == trimmed) {
            out.push(trimmed.to_string());
        }
    }
    out.sort();
    Ok(out)
}

fn normalize_allowed_targets(contract_name: &str, targets: Vec<String>) -> SafeResult<Vec<String>> {
    let mut out = Vec::new();
    for target in targets {
        let trimmed = target.trim();
        if trimmed.is_empty() {
            return Err(SafeError::InvalidVault {
                reason: format!(
                    "contract '{contract_name}': allowed_targets contains an empty target"
                ),
            });
        }
        if trimmed.chars().any(char::is_control) {
            return Err(SafeError::InvalidVault {
                reason: format!(
                    "contract '{contract_name}': target '{trimmed}' contains control characters"
                ),
            });
        }
        if !out.iter().any(|existing: &String| existing == trimmed) {
            out.push(trimmed.to_string());
        }
    }
    out.sort();
    Ok(out)
}

fn validate_contract_name(name: &str) -> SafeResult<()> {
    if name.is_empty() {
        return Err(SafeError::InvalidVault {
            reason: "contract name cannot be empty".into(),
        });
    }
    if !name
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
    {
        return Err(SafeError::InvalidVault {
            reason: format!(
                "contract '{name}': only ASCII letters, digits, '-', '_' and '.' are allowed"
            ),
        });
    }
    Ok(())
}

fn is_json(path: &Path) -> bool {
    path.extension()
        .and_then(|e| e.to_str())
        .map(|e| e == "json")
        .unwrap_or(false)
}

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

    #[test]
    fn parse_yaml_contracts_ignores_other_manifest_sections() {
        let yaml = r#"
pulls:
  - source: akv
    vault_url: https://example.vault.azure.net
contracts:
  deploy:
    profile: work
    namespace: infra
    allowed_secrets:
      - DB_PASSWORD
      - API_KEY
    required_secrets:
      - DB_PASSWORD
    allowed_targets:
      - terraform
      - /usr/bin/tofu
    access_profile: read_only
    trust_level: hardened
    network: restricted
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let contracts = load_contracts(&path).unwrap();
        let deploy = contracts.get("deploy").unwrap();
        assert_eq!(deploy.profile.as_deref(), Some("work"));
        assert_eq!(deploy.namespace.as_deref(), Some("infra"));
        assert_eq!(deploy.allowed_secrets, vec!["API_KEY", "DB_PASSWORD"]);
        assert_eq!(deploy.required_secrets, vec!["DB_PASSWORD"]);
        assert_eq!(deploy.allowed_targets, vec!["/usr/bin/tofu", "terraform"]);
        assert_eq!(deploy.access_profile, RbacProfile::ReadOnly);
        assert_eq!(deploy.network, AuthorityNetworkPolicy::Restricted);
        assert_eq!(
            deploy.resolved_exec_policy().access_profile,
            RbacProfile::ReadOnly
        );
        assert_eq!(
            deploy.resolved_exec_policy().inherit,
            AuthorityInheritMode::Minimal
        );
        assert!(deploy.resolved_exec_policy().deny_dangerous_env);
        assert!(deploy.resolved_exec_policy().redact_output);
    }

    #[test]
    fn parse_json_contract_with_custom_trust() {
        let json = r#"{
  "contracts": {
    "deploy": {
      "allowed_secrets": ["DB_PASSWORD", "DB_PASSWORD"],
      "required_secrets": ["DB_PASSWORD"],
      "trust_level": "custom",
      "inherit": "clean",
      "deny_dangerous_env": true,
      "redact_output": true
    }
  }
}"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.json");
        std::fs::write(&path, json).unwrap();

        let deploy = load_contract(&path, "deploy").unwrap();
        assert_eq!(deploy.allowed_secrets, vec!["DB_PASSWORD"]);
        assert_eq!(
            deploy.trust,
            AuthorityTrust::Custom(CustomAuthorityTrust {
                inherit: AuthorityInheritMode::Clean,
                deny_dangerous_env: true,
                redact_output: true,
            })
        );
    }

    #[test]
    fn missing_trust_level_is_rejected() {
        let yaml = r#"
contracts:
  deploy:
    allowed_secrets: [DB_PASSWORD]
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let err = load_contracts(&path).unwrap_err();
        assert!(matches!(
            err,
            SafeError::InvalidVault { ref reason } if reason.contains("trust_level is required")
        ));
    }

    #[test]
    fn standard_or_hardened_reject_custom_overrides() {
        let yaml = r#"
contracts:
  deploy:
    allowed_secrets: [DB_PASSWORD]
    trust_level: hardened
    inherit: clean
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let err = load_contracts(&path).unwrap_err();
        assert!(matches!(
            err,
            SafeError::InvalidVault { ref reason }
                if reason.contains("only valid with trust_level: custom")
        ));
    }

    #[test]
    fn required_secrets_must_be_allowed() {
        let yaml = r#"
contracts:
  deploy:
    allowed_secrets: [API_KEY]
    required_secrets: [DB_PASSWORD]
    trust_level: standard
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let err = load_contracts(&path).unwrap_err();
        assert!(matches!(
            err,
            SafeError::InvalidVault { ref reason }
                if reason.contains("must also appear in allowed_secrets")
        ));
    }

    #[test]
    fn omitted_allowed_secrets_means_unrestricted_secret_policy() {
        let yaml = r#"
contracts:
  diagnostic:
    required_secrets: [APP_KEY]
    trust_level: standard
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let contract = load_contract(&path, "diagnostic").unwrap();
        assert!(contract.allow_all_secrets);
        assert!(contract.allowed_secrets.is_empty());
        assert_eq!(contract.required_secrets, vec!["APP_KEY"]);
    }

    #[test]
    fn explicit_empty_allowed_secrets_means_no_secret_allowlist() {
        let yaml = r#"
contracts:
  diagnostic:
    allowed_secrets: []
    trust_level: hardened
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let contract = load_contract(&path, "diagnostic").unwrap();
        assert!(!contract.allow_all_secrets);
        assert!(contract.allowed_secrets.is_empty());
    }

    #[test]
    fn zero_secret_diagnostic_contract_is_identified_conservatively() {
        let yaml = r#"
contracts:
  cordance_diagnostic:
    allowed_secrets: []
    required_secrets: []
    trust_level: hardened
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let contract = load_contract(&path, "cordance_diagnostic").unwrap();
        assert!(contract.requires_zero_secrets());
        assert!(contract.injects_zero_secrets());
        assert!(contract.is_no_secret_diagnostic_contract());
    }

    #[test]
    fn omitted_allowed_secrets_is_not_deliberate_zero_secret_diagnostic_shape() {
        let yaml = r#"
contracts:
  missing_required_shape:
    trust_level: hardened
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let contract = load_contract(&path, "missing_required_shape").unwrap();
        assert!(contract.allow_all_secrets);
        assert!(contract.requires_zero_secrets());
        assert!(!contract.injects_zero_secrets());
        assert!(!contract.is_no_secret_diagnostic_contract());
    }

    #[test]
    fn evaluate_target_returns_explicit_decisions() {
        let contract = AuthorityContract {
            name: "deploy".into(),
            profile: None,
            namespace: None,
            access_profile: RbacProfile::ReadWrite,
            allow_all_secrets: false,
            allowed_secrets: vec!["DB_PASSWORD".into()],
            required_secrets: vec!["DB_PASSWORD".into()],
            allowed_targets: vec!["terraform".into(), "/usr/bin/tofu".into()],
            trust: AuthorityTrust::Hardened,
            network: AuthorityNetworkPolicy::Inherit,
        };

        assert_eq!(
            contract.evaluate_target(Some("terraform")),
            AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::AllowedExact,
                matched_allowlist_entry: Some("terraform".into()),
            }
        );
        assert_eq!(
            contract.evaluate_target(Some("/usr/local/bin/terraform")),
            AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::AllowedBasename,
                matched_allowlist_entry: Some("terraform".into()),
            }
        );
        assert_eq!(
            contract.evaluate_target(Some("/usr/bin/tofu")),
            AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::AllowedExact,
                matched_allowlist_entry: Some("/usr/bin/tofu".into()),
            }
        );
        assert_eq!(
            contract.evaluate_target(Some("bash")),
            AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::Denied,
                matched_allowlist_entry: None,
            }
        );
        assert_eq!(
            contract.evaluate_target(None),
            AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::MissingTarget,
                matched_allowlist_entry: None,
            }
        );
        assert!(contract.allows_target("terraform"));
        assert!(!contract.allows_target("bash"));
    }

    #[test]
    fn empty_allowed_targets_are_invalid_for_bound_firewall_by_default() {
        let contract = AuthorityContract {
            name: "deploy".into(),
            profile: None,
            namespace: None,
            access_profile: RbacProfile::ReadWrite,
            allow_all_secrets: false,
            allowed_secrets: vec!["DB_PASSWORD".into()],
            required_secrets: vec!["DB_PASSWORD".into()],
            allowed_targets: Vec::new(),
            trust: AuthorityTrust::Hardened,
            network: AuthorityNetworkPolicy::Inherit,
        };

        assert_eq!(
            validate_bound_firewall_target_policy(&contract),
            Err(BoundFirewallTargetPolicyError::EmptyTargetAllowlist)
        );
    }

    #[test]
    fn empty_allowed_targets_report_blank_scope_for_bound_firewall() {
        let contract = AuthorityContract {
            name: "deploy".into(),
            profile: None,
            namespace: None,
            access_profile: RbacProfile::ReadWrite,
            allow_all_secrets: false,
            allowed_secrets: vec!["DB_PASSWORD".into()],
            required_secrets: vec!["DB_PASSWORD".into()],
            allowed_targets: Vec::new(),
            trust: AuthorityTrust::Hardened,
            network: AuthorityNetworkPolicy::Inherit,
        };

        let err = validate_bound_firewall_target_policy(&contract).unwrap_err();

        assert_eq!(
            err.deny_reason(),
            crate::deny_reason::DenyReason::BlankScope
        );
    }

    #[test]
    fn empty_allowed_targets_are_rejected_even_for_no_secret_diagnostic_contracts() {
        let diagnostic = AuthorityContract {
            name: "cordance_diagnostic".into(),
            profile: None,
            namespace: None,
            access_profile: RbacProfile::ReadOnly,
            allow_all_secrets: false,
            allowed_secrets: Vec::new(),
            required_secrets: Vec::new(),
            allowed_targets: Vec::new(),
            trust: AuthorityTrust::Hardened,
            network: AuthorityNetworkPolicy::Inherit,
        };
        let ambiguous = AuthorityContract {
            allow_all_secrets: true,
            ..diagnostic.clone()
        };

        assert!(diagnostic.is_no_secret_diagnostic_contract());
        assert_eq!(
            validate_bound_firewall_target_policy(&diagnostic),
            Err(BoundFirewallTargetPolicyError::EmptyTargetAllowlist)
        );
        assert!(!ambiguous.is_no_secret_diagnostic_contract());
        assert_eq!(
            validate_bound_firewall_target_policy(&ambiguous),
            Err(BoundFirewallTargetPolicyError::EmptyTargetAllowlist)
        );
    }

    #[test]
    fn zero_secret_diagnostic_contract_still_needs_explicit_bound_target_policy() {
        let diagnostic = AuthorityContract {
            name: "cordance_diagnostic".into(),
            profile: None,
            namespace: None,
            access_profile: RbacProfile::ReadOnly,
            allow_all_secrets: false,
            allowed_secrets: Vec::new(),
            required_secrets: Vec::new(),
            allowed_targets: Vec::new(),
            trust: AuthorityTrust::Hardened,
            network: AuthorityNetworkPolicy::Restricted,
        };

        assert_eq!(
            validate_bound_firewall_target_policy(&diagnostic),
            Err(BoundFirewallTargetPolicyError::EmptyTargetAllowlist)
        );
        assert_eq!(
            validate_bound_firewall_network_policy(&diagnostic, true),
            Ok(())
        );
    }

    #[test]
    fn target_denials_have_deterministic_bound_firewall_reasons() {
        assert_eq!(
            AuthorityTargetDecision::Denied.deny_reason(),
            Some(crate::deny_reason::DenyReason::TargetNotAllowed)
        );
        assert_eq!(
            AuthorityTargetDecision::MissingTarget.deny_reason(),
            Some(crate::deny_reason::DenyReason::TargetMissing)
        );
        assert_eq!(AuthorityTargetDecision::Unconstrained.deny_reason(), None);
        assert_eq!(AuthorityTargetDecision::AllowedExact.deny_reason(), None);
        assert_eq!(AuthorityTargetDecision::AllowedBasename.deny_reason(), None);
    }

    #[test]
    fn filesystem_bound_firewall_denials_keep_stable_reasons() {
        assert_eq!(
            crate::deny_reason::DenyReason::PathEscape.code(),
            "PATH_ESCAPE"
        );
        assert_eq!(
            crate::deny_reason::DenyReason::BadWorkdir.code(),
            "BAD_WORKDIR"
        );
        assert_eq!(
            crate::deny_reason::DenyReason::PathEscape.message(),
            "path escapes the authorized boundary"
        );
        assert_eq!(
            crate::deny_reason::DenyReason::BadWorkdir.message(),
            "workdir is missing, invalid, unsafe, or not canonicalizable"
        );
    }

    #[test]
    fn restricted_network_policy_reports_unenforced_when_host_cannot_enforce() {
        let restricted = AuthorityContract {
            name: "deploy".into(),
            profile: None,
            namespace: None,
            access_profile: RbacProfile::ReadWrite,
            allow_all_secrets: false,
            allowed_secrets: vec!["DB_PASSWORD".into()],
            required_secrets: vec!["DB_PASSWORD".into()],
            allowed_targets: vec!["terraform".into()],
            trust: AuthorityTrust::Hardened,
            network: AuthorityNetworkPolicy::Restricted,
        };
        let inherited = AuthorityContract {
            network: AuthorityNetworkPolicy::Inherit,
            ..restricted.clone()
        };

        assert_eq!(
            validate_bound_firewall_network_policy(&restricted, false),
            Err(crate::deny_reason::DenyReason::NetworkUnenforced)
        );
        assert_eq!(
            validate_bound_firewall_network_policy(&restricted, true),
            Ok(())
        );
        assert_eq!(
            validate_bound_firewall_network_policy(&inherited, false),
            Ok(())
        );
    }

    #[test]
    fn evaluate_target_is_unconstrained_without_allowlist() {
        let contract = AuthorityContract {
            name: "deploy".into(),
            profile: None,
            namespace: None,
            access_profile: RbacProfile::ReadWrite,
            allow_all_secrets: false,
            allowed_secrets: vec!["DB_PASSWORD".into()],
            required_secrets: vec!["DB_PASSWORD".into()],
            allowed_targets: Vec::new(),
            trust: AuthorityTrust::Standard,
            network: AuthorityNetworkPolicy::Inherit,
        };

        assert_eq!(
            contract.evaluate_target(None),
            AuthorityTargetEvaluation {
                decision: AuthorityTargetDecision::Unconstrained,
                matched_allowlist_entry: None,
            }
        );
    }

    #[test]
    fn find_contracts_manifest_uses_repo_search_rules() {
        let dir = tempdir().unwrap();
        let nested = dir.path().join("a/b/c");
        std::fs::create_dir_all(&nested).unwrap();
        let manifest = dir.path().join(".tsafe.yml");
        std::fs::write(&manifest, "contracts: {}").unwrap();

        assert_eq!(find_contracts_manifest(&nested), Some(manifest));
    }

    #[test]
    fn contracts_default_to_read_write_access_profile() {
        let yaml = r#"
contracts:
  deploy:
    allowed_secrets: [DB_PASSWORD]
    trust_level: standard
"#;
        let dir = tempdir().unwrap();
        let path = dir.path().join(".tsafe.yml");
        std::fs::write(&path, yaml).unwrap();

        let deploy = load_contract(&path, "deploy").unwrap();
        assert_eq!(deploy.access_profile, RbacProfile::ReadWrite);
        assert_eq!(
            deploy.resolved_exec_policy().access_profile,
            RbacProfile::ReadWrite
        );
    }
}
