//! Context pack domain model and builder.

use cortex_core::{
    compose_policy_outcomes, AuthorityClass, BoundaryContradictionState, ClaimCeiling,
    ClaimProofState, ContextPackId, ContradictionId, CoreError, CoreResult, DoctrineId, EventId,
    MemoryId, PolicyContribution, PolicyDecision, PolicyOutcome, PrincipleId, ProvenanceClass,
    ReportableClaim, RuntimeMode, SemanticTrustClass,
};
use serde::{Deserialize, Serialize};

use crate::audit::{ExcludedAuditEntry, ExclusionReason, IncludedAuditEntry, SelectionAudit};
use crate::redaction::{PackMode, RawEventPayloadPolicy, RedactionPolicy, Sensitivity};

/// Typed reference included in or considered for a context pack.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ContextRefId {
    /// Durable memory ref.
    Memory {
        /// Memory identifier.
        memory_id: MemoryId,
    },
    /// Principle hypothesis ref.
    Principle {
        /// Principle identifier.
        principle_id: PrincipleId,
    },
    /// Raw event lineage ref.
    Event {
        /// Event identifier.
        event_id: EventId,
    },
}

impl ContextRefId {
    pub(crate) fn kind_name(&self) -> &'static str {
        match self {
            Self::Memory { .. } => "memory",
            Self::Principle { .. } => "principle",
            Self::Event { .. } => "event",
        }
    }
}

/// Ref selected for inclusion in a pack.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SelectedContextRef {
    /// Selected ref id.
    pub ref_id: ContextRefId,
    /// Redacted/abstracted summary safe for the pack's declared policy.
    pub summary: String,
    /// Scope fields describing where this ref applies.
    pub scope: Vec<String>,
    /// Confidence as a percentage, when known.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub confidence: Option<u8>,
    /// Authority tier or source label, when known.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub authority: Option<String>,
    /// Runtime mode bounding this ref's authority claim.
    pub runtime_mode: RuntimeMode,
    /// Authority class used for ceiling calculation.
    pub authority_class: AuthorityClass,
    /// Proof closure state for this ref.
    pub proof_state: ClaimProofState,
    /// Effective claim ceiling after weakest-link downgrades.
    pub claim_ceiling: ClaimCeiling,
    /// Semantic provenance family for this ref.
    pub provenance_class: ProvenanceClass,
    /// Semantic trust class for this ref.
    pub semantic_trust: SemanticTrustClass,
    /// Human-readable downgrade reasons.
    pub downgrade_reasons: Vec<String>,
    /// Why this ref was selected.
    pub selection_reason: String,
    /// Raw event payload, present only for operator-mode explicit opt-in.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub raw_event_payload: Option<serde_json::Value>,
}

/// Candidate selected by an upstream retrieval/memory layer.
#[derive(Debug, Clone, PartialEq)]
pub struct ContextRefCandidate {
    /// Candidate ref id.
    pub ref_id: ContextRefId,
    /// Summary or abstracted content.
    pub summary: String,
    /// Scope fields.
    pub scope: Vec<String>,
    /// Confidence as a percentage, when known.
    pub confidence: Option<u8>,
    /// Authority tier or source label, when known.
    pub authority: Option<String>,
    /// Runtime mode bounding this candidate's authority claim.
    pub runtime_mode: RuntimeMode,
    /// Authority class used for ceiling calculation.
    pub authority_class: AuthorityClass,
    /// Proof closure state for this candidate.
    pub proof_state: ClaimProofState,
    /// Requested claim ceiling before weakest-link downgrade.
    pub requested_ceiling: ClaimCeiling,
    /// Semantic provenance family for this candidate.
    pub provenance_class: ProvenanceClass,
    /// Semantic trust class for this candidate.
    pub semantic_trust: SemanticTrustClass,
    /// Selection reason.
    pub selection_reason: String,
    /// Sensitivity tier used by redaction.
    pub sensitivity: Sensitivity,
    /// Optional raw event payload from upstream.
    pub raw_event_payload: Option<serde_json::Value>,
}

impl ContextRefCandidate {
    /// Build a minimal candidate from a ref and summary.
    #[must_use]
    pub fn new(ref_id: ContextRefId, summary: impl Into<String>) -> Self {
        Self {
            ref_id,
            summary: summary.into(),
            scope: Vec::new(),
            confidence: None,
            authority: None,
            runtime_mode: RuntimeMode::LocalUnsigned,
            authority_class: AuthorityClass::Derived,
            proof_state: ClaimProofState::Unknown,
            requested_ceiling: ClaimCeiling::LocalUnsigned,
            provenance_class: ProvenanceClass::RuntimeDerived,
            semantic_trust: SemanticTrustClass::CandidateOnly,
            selection_reason: "selected_by_caller".to_string(),
            sensitivity: Sensitivity::Internal,
            raw_event_payload: None,
        }
    }

    /// Attach raw event payload supplied by upstream lineage.
    #[must_use]
    pub fn with_raw_event_payload(mut self, raw_event_payload: serde_json::Value) -> Self {
        self.raw_event_payload = Some(raw_event_payload);
        self
    }

    /// Attach sensitivity tier.
    #[must_use]
    pub fn with_sensitivity(mut self, sensitivity: Sensitivity) -> Self {
        self.sensitivity = sensitivity;
        self
    }

    /// Attach explicit claim metadata for context-pack truth ceilings.
    #[must_use]
    pub fn with_claim_metadata(
        mut self,
        runtime_mode: RuntimeMode,
        authority_class: AuthorityClass,
        proof_state: ClaimProofState,
        requested_ceiling: ClaimCeiling,
    ) -> Self {
        self.runtime_mode = runtime_mode;
        self.authority_class = authority_class;
        self.proof_state = proof_state;
        self.requested_ceiling = requested_ceiling;
        self.provenance_class = provenance_from_authority_class(authority_class);
        self.semantic_trust = semantic_trust_from_claim_metadata(authority_class, proof_state);
        self
    }

    /// Attach explicit semantic provenance/trust metadata.
    #[must_use]
    pub const fn with_semantic_metadata(
        mut self,
        provenance_class: ProvenanceClass,
        semantic_trust: SemanticTrustClass,
    ) -> Self {
        self.provenance_class = provenance_class;
        self.semantic_trust = semantic_trust;
        self
    }
}

/// Explicit exclusion recorded on the pack.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackExclusion {
    /// Excluded ref when policy permits recording the identifier.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ref_id: Option<ContextRefId>,
    /// Ref kind retained even when the identifier is redacted.
    pub ref_kind: String,
    /// Why this ref was excluded.
    pub reason: ExclusionReason,
    /// Human-readable rationale.
    pub rationale: String,
}

/// Conflict surfaced to the consumer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackConflict {
    /// Contradiction identifier.
    pub contradiction_id: ContradictionId,
    /// Advisory contradiction posture supplied by retrieval/memory resolution.
    #[serde(default = "default_conflict_posture")]
    pub posture: BoundaryContradictionState,
    /// Refs participating in the conflict.
    pub refs: Vec<ContextRefId>,
    /// Compact conflict summary.
    pub summary: String,
}

/// Bounded injection object with citations and exclusions.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextPack {
    /// Context pack id.
    pub context_pack_id: ContextPackId,
    /// Task this pack was assembled for.
    pub task: String,
    /// Maximum token budget requested by caller.
    pub max_tokens: usize,
    /// Pack mode recorded per BUILD_SPEC §2.2.
    pub pack_mode: PackMode,
    /// Redaction policy recorded per BUILD_SPEC §2.2.
    pub redaction_policy: RedactionPolicy,
    /// Selected memory/principle/event refs.
    pub selected_refs: Vec<SelectedContextRef>,
    /// Active promoted doctrine ids.
    pub active_doctrine_ids: Vec<DoctrineId>,
    /// Surfaced conflicts.
    pub conflicts: Vec<PackConflict>,
    /// Explicit exclusions.
    pub exclusions: Vec<PackExclusion>,
    /// Selection audit for included and excluded refs.
    pub selection_audit: SelectionAudit,
}

impl ContextPack {
    /// Conservative posture for surfaced contradiction state.
    #[must_use]
    pub fn contradiction_posture(&self) -> BoundaryContradictionState {
        if self
            .conflicts
            .iter()
            .any(|conflict| conflict.posture == BoundaryContradictionState::Blocked)
        {
            BoundaryContradictionState::Blocked
        } else if self
            .conflicts
            .iter()
            .any(|conflict| conflict.posture == BoundaryContradictionState::Unknown)
        {
            BoundaryContradictionState::Unknown
        } else if self
            .conflicts
            .iter()
            .any(|conflict| conflict.posture == BoundaryContradictionState::MultiHypothesis)
        {
            BoundaryContradictionState::MultiHypothesis
        } else {
            BoundaryContradictionState::Resolved
        }
    }

    /// Derive the ADR 0026 policy decision for this built pack.
    #[must_use]
    pub fn policy_decision(&self) -> PolicyDecision {
        let mut contributions = vec![PolicyContribution::new(
            "context_pack.builder.valid",
            PolicyOutcome::Allow,
            "context pack built under declared pack/redaction policy",
        )
        .expect("static policy contribution is valid")];

        if self.pack_mode == PackMode::External
            && self.redaction_policy.raw_event_payloads != RawEventPayloadPolicy::Excluded
        {
            contributions.push(
                PolicyContribution::new(
                    "context_pack.redaction.external_raw_payload",
                    PolicyOutcome::Reject,
                    "external context packs must exclude raw event payloads",
                )
                .expect("static policy contribution is valid"),
            );
        }

        if self.pack_mode == PackMode::External
            && self
                .selected_refs
                .iter()
                .any(|selected| selected.raw_event_payload.is_some())
        {
            contributions.push(
                PolicyContribution::new(
                    "context_pack.redaction.raw_payload_leak",
                    PolicyOutcome::Reject,
                    "external context pack selected refs must not carry raw payloads",
                )
                .expect("static policy contribution is valid"),
            );
        }

        if !self.conflicts.is_empty() {
            contributions.push(
                PolicyContribution::new(
                    "context_pack.conflict_present",
                    PolicyOutcome::Quarantine,
                    "context pack contains surfaced conflicts and must not be treated as clean authority",
                )
                .expect("static policy contribution is valid"),
            );
        }

        compose_policy_outcomes(contributions, None)
    }

    /// Fail closed before using this pack in the default external/trusted path.
    ///
    /// Building a pack may still be useful for diagnostics, but default
    /// consumers must not silently use a pack whose composed policy is
    /// `Reject` or `Quarantine`.
    pub fn require_default_use_allowed(&self) -> CoreResult<()> {
        let policy = self.policy_decision();
        match policy.final_outcome {
            PolicyOutcome::Reject | PolicyOutcome::Quarantine => {
                Err(CoreError::Validation(format!(
                    "context pack default use blocked by policy outcome {:?}",
                    policy.final_outcome
                )))
            }
            PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => Ok(()),
        }
    }
}

const fn default_conflict_posture() -> BoundaryContradictionState {
    BoundaryContradictionState::Unknown
}

/// Builder for context packs.
#[derive(Debug, Clone)]
pub struct ContextPackBuilder {
    task: String,
    max_tokens: usize,
    pack_mode: PackMode,
    include_raw_event_payloads: bool,
    selected_candidates: Vec<ContextRefCandidate>,
    active_doctrine_ids: Vec<DoctrineId>,
    conflicts: Vec<PackConflict>,
    exclusions: Vec<ExclusionCandidate>,
}

#[derive(Debug, Clone, PartialEq)]
struct ExclusionCandidate {
    ref_id: ContextRefId,
    reason: ExclusionReason,
    rationale: String,
    sensitivity: Sensitivity,
}

impl ContextPackBuilder {
    /// Create a builder with default external redacted/abstracted policy.
    #[must_use]
    pub fn new(task: impl Into<String>, max_tokens: usize) -> Self {
        Self {
            task: task.into(),
            max_tokens,
            pack_mode: PackMode::External,
            include_raw_event_payloads: false,
            selected_candidates: Vec::new(),
            active_doctrine_ids: Vec::new(),
            conflicts: Vec::new(),
            exclusions: Vec::new(),
        }
    }

    /// Set pack mode. External remains the default.
    #[must_use]
    pub fn pack_mode(mut self, pack_mode: PackMode) -> Self {
        self.pack_mode = pack_mode;
        self
    }

    /// Explicitly opt into raw payloads for operator mode.
    #[must_use]
    pub fn include_raw_event_payloads_in_operator_mode(mut self) -> Self {
        self.include_raw_event_payloads = true;
        self
    }

    /// Add a candidate selected by upstream retrieval/memory logic.
    #[must_use]
    pub fn select_ref(mut self, candidate: ContextRefCandidate) -> Self {
        self.selected_candidates.push(candidate);
        self
    }

    /// Add an active doctrine id.
    #[must_use]
    pub fn active_doctrine(mut self, doctrine_id: DoctrineId) -> Self {
        self.active_doctrine_ids.push(doctrine_id);
        self
    }

    /// Add a surfaced conflict.
    #[must_use]
    pub fn conflict(mut self, conflict: PackConflict) -> Self {
        self.conflicts.push(conflict);
        self
    }

    /// Record an explicitly excluded candidate ref.
    #[must_use]
    pub fn exclude_ref(
        mut self,
        ref_id: ContextRefId,
        reason: ExclusionReason,
        rationale: impl Into<String>,
        sensitivity: Sensitivity,
    ) -> Self {
        self.exclusions.push(ExclusionCandidate {
            ref_id,
            reason,
            rationale: rationale.into(),
            sensitivity,
        });
        self
    }

    /// Build the context pack without model calls or runtime execution.
    pub fn build(self) -> CoreResult<ContextPack> {
        if self.task.trim().is_empty() {
            return Err(CoreError::Validation(
                "context pack task must not be empty".to_string(),
            ));
        }

        if self.max_tokens == 0 {
            return Err(CoreError::Validation(
                "context pack max_tokens must be greater than zero".to_string(),
            ));
        }

        let redaction_policy = self.redaction_policy()?;
        let selected_refs = self.selected_refs(&redaction_policy);
        let exclusions = self.pack_exclusions();
        let estimated_tokens = estimate_pack_tokens(&self.task, &selected_refs, &exclusions);

        if estimated_tokens > self.max_tokens {
            return Err(CoreError::Validation(format!(
                "context pack estimated token count {estimated_tokens} exceeds budget {}",
                self.max_tokens
            )));
        }

        let mut selection_audit =
            SelectionAudit::new(self.pack_mode, redaction_policy.clone(), estimated_tokens);
        selection_audit.included = self.included_audit();
        selection_audit.exclusions = self.excluded_audit();

        Ok(ContextPack {
            context_pack_id: ContextPackId::new(),
            task: self.task,
            max_tokens: self.max_tokens,
            pack_mode: self.pack_mode,
            redaction_policy,
            selected_refs,
            active_doctrine_ids: self.active_doctrine_ids,
            conflicts: self.conflicts,
            exclusions,
            selection_audit,
        })
    }

    fn redaction_policy(&self) -> CoreResult<RedactionPolicy> {
        match (self.pack_mode, self.include_raw_event_payloads) {
            (PackMode::External, false) | (PackMode::Operator, false) => {
                Ok(RedactionPolicy::external_default())
            }
            (PackMode::Operator, true) => Ok(RedactionPolicy::operator_with_raw_payload_opt_in()),
            (PackMode::External, true) => Err(CoreError::Validation(
                "raw event payload opt-in requires operator pack mode".to_string(),
            )),
        }
    }

    fn selected_refs(&self, redaction_policy: &RedactionPolicy) -> Vec<SelectedContextRef> {
        self.selected_candidates
            .iter()
            .map(|candidate| {
                let include_raw_payload = redaction_policy.raw_event_payloads
                    == RawEventPayloadPolicy::OperatorOptIn
                    && candidate.sensitivity != Sensitivity::Secret;
                let claim = ReportableClaim::new(
                    "context pack selected ref",
                    candidate.runtime_mode,
                    candidate.authority_class,
                    candidate.proof_state,
                    candidate.requested_ceiling,
                );
                let claim_ceiling = claim
                    .effective_ceiling()
                    .min(candidate.provenance_class.claim_ceiling())
                    .min(candidate.semantic_trust.claim_ceiling());
                let mut downgrade_reasons = claim.downgrade_reasons().to_vec();
                if claim_ceiling < claim.effective_ceiling() {
                    downgrade_reasons.push(format!(
                        "semantic trust {:?} and provenance {:?} limit authority claims",
                        candidate.semantic_trust, candidate.provenance_class
                    ));
                }
                SelectedContextRef {
                    ref_id: candidate.ref_id.clone(),
                    summary: candidate.summary.clone(),
                    scope: candidate.scope.clone(),
                    confidence: candidate.confidence,
                    authority: candidate.authority.clone(),
                    runtime_mode: claim.runtime_mode(),
                    authority_class: claim.authority_class(),
                    proof_state: claim.proof_state(),
                    claim_ceiling,
                    provenance_class: candidate.provenance_class,
                    semantic_trust: candidate.semantic_trust,
                    downgrade_reasons,
                    selection_reason: candidate.selection_reason.clone(),
                    raw_event_payload: include_raw_payload
                        .then(|| candidate.raw_event_payload.clone())
                        .flatten(),
                }
            })
            .collect()
    }

    fn pack_exclusions(&self) -> Vec<PackExclusion> {
        self.exclusions
            .iter()
            .map(|exclusion| {
                let ref_id = match (self.pack_mode, exclusion.sensitivity) {
                    (PackMode::External, Sensitivity::Personal | Sensitivity::Secret) => None,
                    _ => Some(exclusion.ref_id.clone()),
                };
                PackExclusion {
                    ref_kind: exclusion.ref_id.kind_name().to_string(),
                    ref_id,
                    reason: exclusion.reason,
                    rationale: exclusion.rationale.clone(),
                }
            })
            .collect()
    }

    fn included_audit(&self) -> Vec<IncludedAuditEntry> {
        self.selected_candidates
            .iter()
            .map(|candidate| IncludedAuditEntry {
                ref_id: candidate.ref_id.clone(),
                rule_id: "context_pack.builder.selected_by_caller.v1".to_string(),
                reason: candidate.selection_reason.clone(),
                sensitivity: candidate.sensitivity,
            })
            .collect()
    }

    fn excluded_audit(&self) -> Vec<ExcludedAuditEntry> {
        self.exclusions
            .iter()
            .map(|exclusion| {
                let ref_id = match (self.pack_mode, exclusion.sensitivity) {
                    (PackMode::External, Sensitivity::Personal | Sensitivity::Secret) => None,
                    _ => Some(exclusion.ref_id.clone()),
                };
                ExcludedAuditEntry {
                    ref_kind: exclusion.ref_id.kind_name().to_string(),
                    ref_id,
                    reason: exclusion.reason,
                    rule_id: "context_pack.builder.excluded_by_caller.v1".to_string(),
                    rationale: exclusion.rationale.clone(),
                    sensitivity: exclusion.sensitivity,
                }
            })
            .collect()
    }
}

const fn provenance_from_authority_class(authority_class: AuthorityClass) -> ProvenanceClass {
    match authority_class {
        AuthorityClass::Untrusted => ProvenanceClass::UnknownProvenance,
        AuthorityClass::Derived => ProvenanceClass::RuntimeDerived,
        AuthorityClass::Observed | AuthorityClass::Verified => ProvenanceClass::ToolObserved,
        AuthorityClass::Operator => ProvenanceClass::OperatorAttested,
    }
}

const fn semantic_trust_from_claim_metadata(
    authority_class: AuthorityClass,
    proof_state: ClaimProofState,
) -> SemanticTrustClass {
    match (authority_class, proof_state) {
        (AuthorityClass::Operator, ClaimProofState::FullChainVerified)
        | (
            AuthorityClass::Verified | AuthorityClass::Observed,
            ClaimProofState::FullChainVerified,
        ) => SemanticTrustClass::SingleFamily,
        (AuthorityClass::Derived, _) => SemanticTrustClass::CandidateOnly,
        _ => SemanticTrustClass::Unknown,
    }
}

fn estimate_pack_tokens(
    task: &str,
    selected_refs: &[SelectedContextRef],
    exclusions: &[PackExclusion],
) -> usize {
    let chars = task.len()
        + selected_refs
            .iter()
            .map(|r| r.summary.len() + r.scope.iter().map(String::len).sum::<usize>())
            .sum::<usize>()
        + exclusions
            .iter()
            .map(|e| e.rationale.len() + e.ref_kind.len())
            .sum::<usize>();
    chars.div_ceil(4).max(1)
}

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

    fn event_ref() -> ContextRefId {
        ContextRefId::Event {
            event_id: EventId::new(),
        }
    }

    fn memory_ref() -> ContextRefId {
        ContextRefId::Memory {
            memory_id: MemoryId::new(),
        }
    }

    #[test]
    fn default_external_pack_excludes_raw_event_payload_fields() {
        let pack = ContextPackBuilder::new("prepare safe context", 512)
            .select_ref(
                ContextRefCandidate::new(event_ref(), "operator prefers concise summaries")
                    .with_raw_event_payload(json!({
                        "payload_json": {
                            "private_text": "do not export"
                        }
                    })),
            )
            .build()
            .expect("build external pack");

        let serialized = serde_json::to_value(&pack).expect("serialize pack");
        assert_eq!(serialized["pack_mode"], json!("external"));
        assert_eq!(
            serialized["redaction_policy"]["raw_event_payloads"],
            json!("excluded")
        );
        assert!(!contains_object_key(&serialized, "raw_event_payload"));
        assert!(!contains_object_key(&serialized, "payload_json"));
    }

    #[test]
    fn default_external_pack_records_mode_policy_and_exclusion() {
        let pack = ContextPackBuilder::new("prepare safe context", 512)
            .select_ref(ContextRefCandidate::new(
                memory_ref(),
                "prefer explicit evidence",
            ))
            .exclude_ref(
                event_ref(),
                ExclusionReason::RedactionPolicy,
                "raw private event omitted from external pack",
                Sensitivity::Personal,
            )
            .build()
            .expect("build external pack");

        assert_eq!(pack.pack_mode, PackMode::External);
        assert_eq!(pack.redaction_policy, RedactionPolicy::external_default());
        assert_eq!(pack.exclusions.len(), 1);
        assert_eq!(pack.exclusions[0].reason, ExclusionReason::RedactionPolicy);
        assert_eq!(pack.exclusions[0].ref_id, None);
        assert_eq!(pack.selection_audit.exclusions.len(), 1);
        assert_eq!(pack.selection_audit.exclusions[0].ref_id, None);
        assert_eq!(pack.selection_audit.pack_mode, PackMode::External);
        assert_eq!(
            pack.selection_audit.redaction_policy,
            RedactionPolicy::external_default()
        );
        assert_eq!(pack.policy_decision().final_outcome, PolicyOutcome::Allow);
    }

    #[test]
    fn selected_refs_expose_truth_ceiling_metadata() {
        let pack = ContextPackBuilder::new("prepare safe context", 512)
            .select_ref(ContextRefCandidate::new(
                memory_ref(),
                "prefer explicit evidence",
            ))
            .build()
            .expect("build external pack");

        let selected = &pack.selected_refs[0];
        assert_eq!(selected.runtime_mode, RuntimeMode::LocalUnsigned);
        assert_eq!(selected.authority_class, AuthorityClass::Derived);
        assert_eq!(selected.proof_state, ClaimProofState::Unknown);
        assert_eq!(selected.claim_ceiling, ClaimCeiling::DevOnly);
        assert_eq!(selected.provenance_class, ProvenanceClass::RuntimeDerived);
        assert_eq!(selected.semantic_trust, SemanticTrustClass::CandidateOnly);
        assert!(selected
            .downgrade_reasons
            .iter()
            .any(|reason| reason.contains("proof state Unknown")));

        let serialized = serde_json::to_value(&pack).expect("serialize pack");
        assert_eq!(
            serialized["selected_refs"][0]["proof_state"],
            json!("unknown")
        );
        assert_eq!(
            serialized["selected_refs"][0]["claim_ceiling"],
            json!("dev_only")
        );
        assert_eq!(
            serialized["selected_refs"][0]["provenance_class"],
            json!("runtime_derived")
        );
        assert_eq!(
            serialized["selected_refs"][0]["semantic_trust"],
            json!("candidate_only")
        );
    }

    #[test]
    fn selected_refs_expose_explicit_semantic_trust_metadata() {
        let pack = ContextPackBuilder::new("prepare safe context", 512)
            .select_ref(
                ContextRefCandidate::new(memory_ref(), "operator falsified support")
                    .with_claim_metadata(
                        RuntimeMode::AuthorityGrade,
                        AuthorityClass::Operator,
                        ClaimProofState::FullChainVerified,
                        ClaimCeiling::AuthorityGrade,
                    )
                    .with_semantic_metadata(
                        ProvenanceClass::OperatorAttested,
                        SemanticTrustClass::FalsificationTested,
                    ),
            )
            .build()
            .expect("build external pack");

        let selected = &pack.selected_refs[0];
        assert_eq!(selected.provenance_class, ProvenanceClass::OperatorAttested);
        assert_eq!(
            selected.semantic_trust,
            SemanticTrustClass::FalsificationTested
        );
        assert_eq!(selected.claim_ceiling, ClaimCeiling::AuthorityGrade);
    }

    #[test]
    fn conflict_pack_policy_decision_is_quarantine() {
        let ref_id = memory_ref();
        let pack = ContextPackBuilder::new("prepare conflicted context", 512)
            .select_ref(ContextRefCandidate::new(
                ref_id.clone(),
                "conflicted memory",
            ))
            .conflict(PackConflict {
                contradiction_id: ContradictionId::new(),
                posture: BoundaryContradictionState::Blocked,
                refs: vec![ref_id],
                summary: "memory conflicts with a newer claim".into(),
            })
            .build()
            .expect("build pack");

        let policy = pack.policy_decision();

        assert_eq!(policy.final_outcome, PolicyOutcome::Quarantine);
        assert_eq!(
            policy.contributing[0].rule_id.as_str(),
            "context_pack.conflict_present"
        );
    }

    #[test]
    fn conflict_pack_fails_closed_for_default_use() {
        let ref_id = memory_ref();
        let pack = ContextPackBuilder::new("prepare conflicted context", 512)
            .select_ref(ContextRefCandidate::new(
                ref_id.clone(),
                "conflicted memory",
            ))
            .conflict(PackConflict {
                contradiction_id: ContradictionId::new(),
                posture: BoundaryContradictionState::Blocked,
                refs: vec![ref_id],
                summary: "memory conflicts with a newer claim".into(),
            })
            .build()
            .expect("build diagnostic pack");

        let err = pack
            .require_default_use_allowed()
            .expect_err("conflicted pack must not be default-usable");
        assert!(
            err.to_string().contains("Quarantine"),
            "default-use failure should expose the policy outcome: {err}"
        );
    }

    #[test]
    fn clean_external_pack_is_default_usable() {
        let pack = ContextPackBuilder::new("prepare safe context", 512)
            .select_ref(ContextRefCandidate::new(
                memory_ref(),
                "prefer explicit evidence",
            ))
            .build()
            .expect("build external pack");

        pack.require_default_use_allowed()
            .expect("clean pack is default-usable");
    }

    #[test]
    fn conflict_posture_carries_unknown_and_multi_hypothesis_advisories() {
        let unknown_ref = memory_ref();
        let multi_hypothesis_ref = event_ref();
        let pack = ContextPackBuilder::new("prepare conflicted context", 768)
            .select_ref(ContextRefCandidate::new(
                unknown_ref.clone(),
                "conflict resolver could not prove precedence",
            ))
            .conflict(PackConflict {
                contradiction_id: ContradictionId::new(),
                posture: BoundaryContradictionState::Unknown,
                refs: vec![unknown_ref],
                summary: "precedence proof is missing".into(),
            })
            .conflict(PackConflict {
                contradiction_id: ContradictionId::new(),
                posture: BoundaryContradictionState::MultiHypothesis,
                refs: vec![multi_hypothesis_ref],
                summary: "both hypotheses remain live".into(),
            })
            .build()
            .expect("build diagnostic pack");

        assert_eq!(
            pack.contradiction_posture(),
            BoundaryContradictionState::Unknown
        );
        assert_eq!(
            pack.policy_decision().final_outcome,
            PolicyOutcome::Quarantine
        );

        let serialized = serde_json::to_value(&pack).expect("serialize pack");
        assert_eq!(serialized["conflicts"][0]["posture"], json!("unknown"));
        assert_eq!(
            serialized["conflicts"][1]["posture"],
            json!("multi_hypothesis")
        );
    }

    fn contains_object_key(value: &serde_json::Value, needle: &str) -> bool {
        match value {
            serde_json::Value::Object(map) => map
                .iter()
                .any(|(key, value)| key == needle || contains_object_key(value, needle)),
            serde_json::Value::Array(values) => values
                .iter()
                .any(|value| contains_object_key(value, needle)),
            _ => false,
        }
    }
}
