mod model;
mod presentation;

use super::gap_decision_ledger::{self, GapRecord};
use model::*;
pub(crate) use model::{GateEvaluateInput, GateMode};
pub(crate) use presentation::{
    gate_decision_should_fail, gate_decision_status, markdown_path_for, render_gate_decision_json,
    render_gate_decision_markdown,
};
use serde_json::Value;
#[cfg(test)]
use serde_json::json;
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};

pub(crate) const DEFAULT_GATE_OUT: &str = "target/ripr/reports/gate-decision.json";
const SCHEMA_VERSION: &str = "0.1";
const DEFAULT_THRESHOLD: &str = "high_confidence_new_gap";
const DEFAULT_ACKNOWLEDGEMENT_LABEL: &str = "ripr-waive";
const LIMITS_NOTE: &str = "Optional policy over static RIPR evidence; advisory by default; runtime mutation calibration is used only when supplied.";

pub(crate) fn build_gate_decision_report(
    input: &GateEvaluateInput,
) -> Result<GateDecisionReport, String> {
    let mut warnings = Vec::new();
    let mut config_errors = Vec::new();
    let labels = read_labels(input, &mut warnings)?;
    if input.pr_guidance.is_none() && input.gap_ledger.is_none() {
        config_errors
            .push("gate evaluate requires --pr-guidance <path> or --gap-ledger <path>".to_string());
    }
    let pr_guidance = match input.pr_guidance.as_ref() {
        Some(path) => {
            let pr_guidance_path = resolve_root_path(&input.root, path);
            match read_json_value_with_display(&pr_guidance_path, path) {
                Ok(value) => value,
                Err(error) => {
                    config_errors.push(format!(
                        "required PR guidance input {} is invalid: {error}",
                        display_path(path)
                    ));
                    Value::Null
                }
            }
        }
        None => Value::Null,
    };
    let gap_ledger = read_gap_ledger(input, &mut config_errors);
    warn_for_optional_json(
        &input.root,
        input.repo_exposure.as_ref(),
        "repo_exposure",
        &mut warnings,
    );
    warn_for_optional_json(
        &input.root,
        input.sarif_policy.as_ref(),
        "sarif_policy",
        &mut warnings,
    );
    warn_for_optional_json(
        &input.root,
        input.agent_verify.as_ref(),
        "agent_verify",
        &mut warnings,
    );
    warn_for_optional_json(
        &input.root,
        input.agent_receipt.as_ref(),
        "agent_receipt",
        &mut warnings,
    );
    warn_for_optional_json(
        &input.root,
        input.mutation_calibration.as_ref(),
        "mutation_calibration",
        &mut warnings,
    );

    let recommendation_calibration = read_recommendation_calibration(input, &mut warnings);
    let mutation_calibration = read_mutation_calibration(input, &mut warnings);
    let baseline = read_baseline(input, &mut warnings, &mut config_errors);
    let candidates = if config_errors.is_empty() {
        if let Some(records) = gap_ledger.as_ref() {
            candidates_from_gap_ledger(records)
        } else {
            candidates_from_pr_guidance(&pr_guidance)
        }
    } else {
        Vec::new()
    };
    let policy = GatePolicy {
        mode: input.mode,
        threshold: DEFAULT_THRESHOLD.to_string(),
        acknowledgement_labels: acknowledgement_labels(input),
        default_workflow_posture: "advisory".to_string(),
    };
    let mut decisions = candidates
        .iter()
        .map(|candidate| {
            gate_decision(
                candidate,
                &policy,
                &labels,
                &recommendation_calibration,
                &mutation_calibration,
                &baseline,
            )
        })
        .collect::<Vec<_>>();
    decisions.sort_by(|left, right| left.id.cmp(&right.id));
    let summary = summarize_decisions(&decisions);
    let status = top_level_status(&summary, &warnings, &config_errors, input.mode).to_string();
    Ok(GateDecisionReport {
        status,
        mode: input.mode,
        root: display_path(&input.root),
        inputs: GateDecisionInputs {
            repo_exposure: input.repo_exposure.as_ref().map(|path| display_path(path)),
            pr_guidance: input.pr_guidance.as_ref().map(|path| display_path(path)),
            gap_ledger: input.gap_ledger.as_ref().map(|path| display_path(path)),
            sarif_policy: input.sarif_policy.as_ref().map(|path| display_path(path)),
            labels_json: input.labels_json.as_ref().map(|path| display_path(path)),
            labels,
            agent_verify: input.agent_verify.as_ref().map(|path| display_path(path)),
            agent_receipt: input.agent_receipt.as_ref().map(|path| display_path(path)),
            recommendation_calibration: input
                .recommendation_calibration
                .as_ref()
                .map(|path| display_path(path)),
            mutation_calibration: input
                .mutation_calibration
                .as_ref()
                .map(|path| display_path(path)),
            baseline: input.baseline.as_ref().map(|path| display_path(path)),
        },
        policy,
        summary,
        decisions,
        warnings,
        config_errors,
    })
}

fn read_labels(
    input: &GateEvaluateInput,
    warnings: &mut Vec<String>,
) -> Result<Vec<String>, String> {
    let mut labels = input
        .labels
        .iter()
        .filter(|label| !label.trim().is_empty())
        .cloned()
        .collect::<BTreeSet<_>>();
    if let Some(path) = &input.labels_json {
        let resolved = resolve_root_path(&input.root, path);
        match read_json_value_with_display(&resolved, path) {
            Ok(value) => {
                for label in labels_from_value(&value) {
                    labels.insert(label);
                }
            }
            Err(error) => warnings.push(format!(
                "optional labels_json {} is unavailable: {error}",
                display_path(path)
            )),
        }
    }
    Ok(labels.into_iter().collect())
}

fn labels_from_value(value: &Value) -> Vec<String> {
    if let Some(values) = value.as_array() {
        return values
            .iter()
            .filter_map(Value::as_str)
            .map(ToOwned::to_owned)
            .collect();
    }
    value
        .get("labels")
        .and_then(Value::as_array)
        .map(|values| {
            values
                .iter()
                .filter_map(Value::as_str)
                .map(ToOwned::to_owned)
                .collect()
        })
        .unwrap_or_default()
}

fn warn_for_optional_json(
    root: &Path,
    path: Option<&PathBuf>,
    name: &str,
    warnings: &mut Vec<String>,
) {
    let Some(path) = path else {
        return;
    };
    if let Err(error) = read_json_value_with_display(&resolve_root_path(root, path), path) {
        warnings.push(format!(
            "optional {name} {} is unavailable: {error}",
            display_path(path)
        ));
    }
}

fn read_gap_ledger(
    input: &GateEvaluateInput,
    config_errors: &mut Vec<String>,
) -> Option<Vec<GapRecord>> {
    let path = input.gap_ledger.as_ref()?;
    let resolved = resolve_root_path(&input.root, path);
    let text = match fs::read_to_string(&resolved) {
        Ok(text) => text,
        Err(error) => {
            config_errors.push(format!(
                "required gap decision ledger input {} is invalid: read failed: {error}",
                display_path(path)
            ));
            return Some(Vec::new());
        }
    };
    match gap_decision_ledger::parse_gap_records_json(&text) {
        Ok(records) => Some(records),
        Err(error) => {
            config_errors.push(format!(
                "required gap decision ledger input {} is invalid: {error}",
                display_path(path)
            ));
            Some(Vec::new())
        }
    }
}

fn read_recommendation_calibration(
    input: &GateEvaluateInput,
    warnings: &mut Vec<String>,
) -> CalibrationIndex {
    let mut index = CalibrationIndex::default();
    let Some(path) = &input.recommendation_calibration else {
        return index;
    };
    let resolved = resolve_root_path(&input.root, path);
    let value = match read_json_value_with_display(&resolved, path) {
        Ok(value) => value,
        Err(error) => {
            warnings.push(format!(
                "optional recommendation_calibration {} is unavailable: {error}",
                display_path(path)
            ));
            return index;
        }
    };
    for item in value
        .get("recommendations")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        let evidence = CalibrationEvidence {
            available: true,
            outcome: string_field(item.pointer("/calibration/outcome")),
            confidence_effect: recommendation_confidence_effect(
                item.pointer("/calibration/outcome").and_then(Value::as_str),
            )
            .to_string(),
        };
        if let Some(id) = item.get("id").and_then(Value::as_str) {
            index.by_source_id.insert(id.to_string(), evidence.clone());
        }
        if let Some(seam_id) = item.get("seam_id").and_then(Value::as_str) {
            index.by_seam_id.insert(seam_id.to_string(), evidence);
        }
    }
    index
}

fn read_mutation_calibration(
    input: &GateEvaluateInput,
    warnings: &mut Vec<String>,
) -> CalibrationIndex {
    let mut index = CalibrationIndex::default();
    let Some(path) = &input.mutation_calibration else {
        return index;
    };
    let resolved = resolve_root_path(&input.root, path);
    let value = match read_json_value_with_display(&resolved, path) {
        Ok(value) => value,
        Err(error) => {
            warnings.push(format!(
                "optional mutation_calibration {} is unavailable: {error}",
                display_path(path)
            ));
            return index;
        }
    };
    for item in value
        .get("matches")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        let seam_id = item
            .pointer("/static/seam_id")
            .and_then(Value::as_str)
            .or_else(|| item.pointer("/runtime/seam_id").and_then(Value::as_str));
        let Some(seam_id) = seam_id else {
            continue;
        };
        let outcome = item
            .pointer("/runtime/runtime_outcome")
            .and_then(Value::as_str)
            .or_else(|| item.pointer("/runtime/outcome").and_then(Value::as_str));
        index.by_seam_id.insert(
            seam_id.to_string(),
            CalibrationEvidence {
                available: true,
                outcome: outcome.map(ToOwned::to_owned),
                confidence_effect: mutation_confidence_effect(outcome).to_string(),
            },
        );
    }
    for item in value
        .get("static_only_findings")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        if let Some(seam_id) = item.pointer("/static/seam_id").and_then(Value::as_str) {
            index.by_seam_id.insert(
                seam_id.to_string(),
                CalibrationEvidence {
                    available: true,
                    outcome: Some("static_gap_without_runtime_signal".to_string()),
                    confidence_effect: "keeps_advisory".to_string(),
                },
            );
        }
    }
    if !value
        .get("ambiguous_file_line_matches")
        .and_then(Value::as_array)
        .map(|items| items.is_empty())
        .unwrap_or(true)
    {
        warnings.push(format!(
            "mutation_calibration {} contains ambiguous file/line matches; those records do not raise gate confidence",
            display_path(path)
        ));
    }
    index
}

fn recommendation_confidence_effect(outcome: Option<&str>) -> &'static str {
    match outcome {
        Some("useful" | "summary_only_correct" | "suppressed_correctly") => "supports_static_gap",
        Some("noisy" | "wrong_line" | "wrong_target" | "already_covered") => "keeps_advisory",
        Some(_) => "unknown",
        None => "not_used",
    }
}

fn mutation_confidence_effect(outcome: Option<&str>) -> &'static str {
    let Some(outcome) = outcome else {
        return "not_used";
    };
    if is_runtime_gap_outcome(outcome) {
        "supports_static_gap"
    } else if matches!(
        outcome,
        "caught" | "timeout" | "static_gap_without_runtime_signal"
    ) {
        "keeps_advisory"
    } else {
        "unknown"
    }
}

fn is_runtime_gap_outcome(outcome: &str) -> bool {
    outcome == "missed"
        || outcome == "not_caught"
        || outcome == "uncaught"
        || outcome == format!("{}{}", "sur", "vived")
}

fn read_baseline(
    input: &GateEvaluateInput,
    warnings: &mut Vec<String>,
    config_errors: &mut Vec<String>,
) -> BaselineIndex {
    if input.mode.requires_baseline() && input.baseline.is_none() {
        config_errors.push(format!(
            "{} mode requires an explicit --baseline artifact",
            input.mode.as_str()
        ));
        return BaselineIndex::default();
    }
    let Some(path) = &input.baseline else {
        return BaselineIndex::default();
    };
    let resolved = resolve_root_path(&input.root, path);
    match read_json_value_with_display(&resolved, path) {
        Ok(value) => baseline_index_from_value(&value),
        Err(error) if input.mode.requires_baseline() => {
            config_errors.push(format!(
                "required baseline {} is invalid: {error}",
                display_path(path)
            ));
            BaselineIndex::default()
        }
        Err(error) => {
            warnings.push(format!(
                "optional baseline {} is unavailable: {error}",
                display_path(path)
            ));
            BaselineIndex::default()
        }
    }
}

fn baseline_index_from_value(value: &Value) -> BaselineIndex {
    let mut index = BaselineIndex::default();
    for item in value
        .get("entries")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        collect_identity(&mut index.identities, item.get("canonical_gap_id"));
        collect_identity(
            &mut index.identities,
            item.pointer("/identity/canonical_gap_id"),
        );
        collect_identity(&mut index.identities, item.pointer("/identity/seam_id"));
        collect_identity(&mut index.identities, item.pointer("/identity/source_id"));
        collect_identity(&mut index.identities, item.pointer("/identity/id"));
        collect_identity(&mut index.identities, item.pointer("/identity/dedupe_key"));
        collect_identity(&mut index.identities, item.pointer("/identity/fallback"));
        collect_identity(
            &mut index.identities,
            item.pointer("/evidence_record/canonical_gap_id"),
        );
    }
    for item in value
        .get("decisions")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        collect_identity(&mut index.identities, item.get("canonical_gap_id"));
        collect_identity(
            &mut index.identities,
            item.pointer("/identity/canonical_gap_id"),
        );
        collect_identity(
            &mut index.identities,
            item.pointer("/evidence_record/canonical_gap_id"),
        );
        collect_identity(&mut index.identities, item.get("seam_id"));
        collect_identity(&mut index.identities, item.get("source_id"));
    }
    for collection in ["comments", "summary_only", "suppressed"] {
        for item in value
            .get(collection)
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
        {
            collect_identity(&mut index.identities, item.get("canonical_gap_id"));
            collect_identity(
                &mut index.identities,
                item.pointer("/identity/canonical_gap_id"),
            );
            collect_identity(
                &mut index.identities,
                item.pointer("/evidence_record/canonical_gap_id"),
            );
            collect_identity(&mut index.identities, item.get("seam_id"));
            collect_identity(&mut index.identities, item.get("id"));
            collect_identity(&mut index.identities, item.get("dedupe_key"));
        }
    }
    index
}

fn collect_identity(identities: &mut BTreeSet<String>, value: Option<&Value>) {
    if let Some(text) = value
        .and_then(Value::as_str)
        .filter(|text| !text.is_empty())
    {
        identities.insert(text.to_string());
    }
}

fn candidates_from_pr_guidance(value: &Value) -> Vec<GateCandidate> {
    let nearby_test_changed = value
        .pointer("/summary/unchanged_tests")
        .and_then(Value::as_bool)
        .map(|unchanged| !unchanged)
        .unwrap_or(false);
    let mut candidates = Vec::new();
    for source in ["comments", "summary_only"] {
        for item in value
            .get(source)
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
        {
            candidates.push(candidate_from_guidance_item(
                source,
                item,
                nearby_test_changed,
                false,
            ));
        }
    }
    for item in value
        .get("suppressed")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        candidates.push(candidate_from_guidance_item(
            "suppressed",
            item,
            nearby_test_changed,
            true,
        ));
    }
    candidates
}

fn candidates_from_gap_ledger(records: &[GapRecord]) -> Vec<GateCandidate> {
    records.iter().map(candidate_from_gap_record).collect()
}

fn candidate_from_guidance_item(
    source: &str,
    item: &Value,
    nearby_test_changed: bool,
    suppressed: bool,
) -> GateCandidate {
    let source_id = item
        .get("id")
        .and_then(Value::as_str)
        .or_else(|| item.get("dedupe_key").and_then(Value::as_str))
        .or_else(|| item.get("seam_id").and_then(Value::as_str))
        .unwrap_or("unknown")
        .to_string();
    let placement = GatePlacement {
        path: string_field(item.pointer("/placement/path"))
            .or_else(|| string_field(item.pointer("/seam/file"))),
        line: item
            .pointer("/placement/line")
            .and_then(Value::as_u64)
            .or_else(|| item.pointer("/seam/line").and_then(Value::as_u64)),
    };
    let recommended_file = item
        .pointer("/suggested_test/recommended_file")
        .and_then(Value::as_str);
    let near_test = item
        .pointer("/suggested_test/near_test")
        .and_then(Value::as_str);
    let recommended_test = match (recommended_file, near_test) {
        (Some(file), Some(test)) => Some(format!("{file}::{test}")),
        (Some(file), None) => Some(file.to_string()),
        (None, Some(test)) => Some(test.to_string()),
        (None, None) => None,
    };
    let suppression_reason = item
        .get("reason")
        .and_then(Value::as_str)
        .or_else(|| item.get("suppression_reason").and_then(Value::as_str))
        .map(ToOwned::to_owned);
    GateCandidate {
        source: source.to_string(),
        source_id,
        gap_id: None,
        gap_kind: None,
        canonical_gap_id: canonical_gap_id_from_value(item),
        seam_id: string_field(item.get("seam_id")),
        static_class: string_field(item.get("grip_class"))
            .or_else(|| string_field(item.get("class"))),
        severity: string_field(item.get("severity")),
        placement,
        missing_discriminator: string_field(item.get("missing_discriminator")),
        assertion_shape: string_field(item.pointer("/suggested_test/assertion_shape")),
        candidate_values: item
            .pointer("/suggested_test/candidate_values")
            .and_then(Value::as_array)
            .map(|values| {
                values
                    .iter()
                    .filter_map(Value::as_str)
                    .map(ToOwned::to_owned)
                    .collect()
            })
            .unwrap_or_default(),
        recommended_test,
        repair_route: None,
        verification_commands: Vec::new(),
        nearby_test_changed,
        suppressed,
        configured_off: suppression_reason.as_deref() == Some("severity_off"),
        suppression_reason,
        gap_ledger_gate_candidate: false,
        gap_ledger_gate_reason: None,
        gap_ledger_safe_gate_predicate: false,
    }
}

fn candidate_from_gap_record(record: &GapRecord) -> GateCandidate {
    let gap_id = non_empty_string(&record.gap_id);
    let canonical_gap_id = non_empty_string(&record.canonical_gap_id);
    let source_id = gap_id
        .clone()
        .or_else(|| canonical_gap_id.clone())
        .unwrap_or_else(|| "unknown".to_string());
    let projection = record.projection_eligibility.get("gate_candidate");
    let gate_candidate = projection.is_some_and(|projection| projection.eligible);
    let gate_reason = projection
        .and_then(|projection| non_empty_string(&projection.reason))
        .or_else(|| Some("not_gate_candidate".to_string()));
    let repair_route = record.repair_route.clone();
    let changed_behavior = repair_route
        .as_ref()
        .and_then(|route| route.changed_behavior.clone());
    let assertion_shape = repair_route
        .as_ref()
        .and_then(|route| route.assertion_shape.clone());
    let recommended_test = repair_route
        .as_ref()
        .and_then(|route| route.related_test.clone())
        .or_else(|| {
            repair_route
                .as_ref()
                .and_then(|route| route.target_file.clone())
        });
    let placement = GatePlacement {
        path: record
            .anchor
            .as_ref()
            .and_then(|anchor| anchor.file.clone()),
        line: record.anchor.as_ref().and_then(|anchor| anchor.line),
    };
    GateCandidate {
        source: "gap_decision_ledger".to_string(),
        source_id,
        gap_id,
        gap_kind: non_empty_string(&record.kind),
        canonical_gap_id,
        seam_id: None,
        static_class: non_empty_string(&record.kind),
        severity: Some("warning".to_string()),
        placement,
        missing_discriminator: changed_behavior.clone(),
        assertion_shape,
        candidate_values: Vec::new(),
        recommended_test,
        repair_route,
        verification_commands: record.verification_commands.clone(),
        nearby_test_changed: false,
        suppressed: record.policy_state == "suppressed"
            || record
                .safe_gate_predicate
                .as_ref()
                .is_some_and(|predicate| predicate.suppressed),
        configured_off: record.policy_state == "not_policy_targeted",
        suppression_reason: (record.policy_state == "suppressed").then(|| "suppressed".to_string()),
        gap_ledger_gate_candidate: gate_candidate,
        gap_ledger_gate_reason: gate_reason,
        gap_ledger_safe_gate_predicate: gap_decision_ledger::safe_gate_predicate_satisfied(record),
    }
}

fn gate_decision(
    candidate: &GateCandidate,
    policy: &GatePolicy,
    labels: &[String],
    recommendation_calibration: &CalibrationIndex,
    mutation_calibration: &CalibrationIndex,
    baseline: &BaselineIndex,
) -> GateDecision {
    let recommendation_calibration =
        calibration_for_candidate(candidate, recommendation_calibration);
    let mutation_calibration = calibration_for_candidate(candidate, mutation_calibration);
    let eligible = candidate_is_policy_eligible(candidate);
    let baseline_identity = baseline_identity(candidate);
    let is_baseline_new = baseline_identity
        .as_ref()
        .map(|identity| !baseline.identities.contains(identity))
        .unwrap_or(true);
    let acknowledgement_label = acknowledgement_label(policy, labels);
    let would_block = candidate_would_block(
        candidate,
        policy.mode,
        eligible,
        is_baseline_new,
        &recommendation_calibration,
        &mutation_calibration,
    );
    let decision = if candidate.suppressed || candidate.configured_off {
        "suppressed"
    } else if !eligible
        && (candidate.static_class.is_none() || candidate.source == "gap_decision_ledger")
    {
        "not_applicable"
    } else if would_block && acknowledgement_label.is_some() {
        "acknowledged"
    } else if would_block {
        "blocking"
    } else {
        "advisory"
    }
    .to_string();
    let gate_reason = gate_reason(
        candidate,
        GateReasonContext {
            mode: policy.mode,
            decision: &decision,
            eligible,
            is_baseline_new,
            recommendation_calibration: &recommendation_calibration,
            mutation_calibration: &mutation_calibration,
            acknowledgement_label: acknowledgement_label.as_deref(),
        },
    );
    GateDecision {
        id: format!("ripr-gate-{}", stable_identity(candidate)),
        source: if candidate.source == "summary_only" {
            "pr_guidance_summary".to_string()
        } else if candidate.source == "gap_decision_ledger" {
            "gap_decision_ledger".to_string()
        } else {
            "pr_guidance".to_string()
        },
        decision,
        gate_reason,
        gap_id: candidate.gap_id.clone(),
        gap_kind: candidate.gap_kind.clone(),
        canonical_gap_id: candidate.canonical_gap_id.clone(),
        seam_id: candidate.seam_id.clone(),
        source_id: candidate.source_id.clone(),
        static_class: candidate.static_class.clone(),
        severity: candidate.severity.clone(),
        placement: candidate.placement.clone(),
        policy: GateDecisionPolicy {
            mode: policy.mode,
            threshold: policy.threshold.clone(),
            acknowledgement_label,
            baseline_identity,
        },
        evidence: GateEvidence {
            missing_discriminator: candidate.missing_discriminator.clone(),
            assertion_shape: candidate.assertion_shape.clone(),
            candidate_values: candidate.candidate_values.clone(),
            recommended_test: candidate.recommended_test.clone(),
            repair_route: candidate.repair_route.clone(),
            verification_commands: candidate.verification_commands.clone(),
            nearby_test_changed: candidate.nearby_test_changed,
            suppressed: candidate.suppressed,
            configured_off: candidate.configured_off,
            recommendation_calibration,
            mutation_calibration,
        },
    }
}

fn calibration_for_candidate(
    candidate: &GateCandidate,
    calibration: &CalibrationIndex,
) -> CalibrationEvidence {
    candidate
        .seam_id
        .as_ref()
        .and_then(|seam_id| calibration.by_seam_id.get(seam_id))
        .or_else(|| calibration.by_source_id.get(&candidate.source_id))
        .cloned()
        .unwrap_or_else(|| CalibrationEvidence {
            available: false,
            outcome: None,
            confidence_effect: "not_used".to_string(),
        })
}

fn candidate_is_policy_eligible(candidate: &GateCandidate) -> bool {
    if candidate.source == "gap_decision_ledger" {
        return !candidate.suppressed
            && !candidate.configured_off
            && candidate.gap_ledger_gate_candidate
            && candidate.gap_ledger_safe_gate_predicate
            && candidate.repair_route.is_some()
            && !candidate.verification_commands.is_empty()
            && candidate.placement.path.is_some()
            && candidate.placement.line.is_some();
    }
    !candidate.suppressed
        && !candidate.configured_off
        && candidate_class_is_policy_eligible(candidate.static_class.as_deref())
        && has_concrete_guidance(candidate)
        && !candidate.nearby_test_changed
        && candidate.placement.path.is_some()
        && candidate.placement.line.is_some()
        && candidate.source != "summary_only"
}

fn candidate_class_is_policy_eligible(class: Option<&str>) -> bool {
    matches!(
        class,
        Some("weakly_gripped" | "ungripped" | "reachable_unrevealed" | "weakly_exposed")
    )
}

fn has_concrete_guidance(candidate: &GateCandidate) -> bool {
    candidate.missing_discriminator.is_some()
        || candidate.assertion_shape.is_some()
        || !candidate.candidate_values.is_empty()
        || candidate.recommended_test.is_some()
}

fn baseline_identity(candidate: &GateCandidate) -> Option<String> {
    candidate
        .canonical_gap_id
        .clone()
        .or_else(|| candidate.gap_id.clone())
        .or_else(|| candidate.seam_id.clone())
        .or_else(|| (!candidate.source_id.is_empty()).then(|| candidate.source_id.clone()))
        .or_else(|| {
            Some(format!(
                "{}:{}:{}",
                candidate.placement.path.as_deref()?,
                candidate.placement.line?,
                candidate.static_class.as_deref().unwrap_or("unknown")
            ))
        })
}

fn stable_identity(candidate: &GateCandidate) -> String {
    baseline_identity(candidate)
        .unwrap_or_else(|| candidate.source_id.clone())
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
                ch
            } else {
                '-'
            }
        })
        .collect()
}

fn acknowledgement_label(policy: &GatePolicy, labels: &[String]) -> Option<String> {
    policy
        .acknowledgement_labels
        .iter()
        .find(|label| labels.iter().any(|present| present == *label))
        .cloned()
}

fn candidate_would_block(
    candidate: &GateCandidate,
    mode: GateMode,
    eligible: bool,
    is_baseline_new: bool,
    recommendation_calibration: &CalibrationEvidence,
    mutation_calibration: &CalibrationEvidence,
) -> bool {
    if !eligible {
        return false;
    }
    match mode {
        GateMode::VisibleOnly => false,
        GateMode::Acknowledgeable => true,
        GateMode::BaselineCheck => is_baseline_new,
        GateMode::CalibratedGate => {
            is_baseline_new
                && (recommendation_calibration.confidence_effect == "supports_static_gap"
                    || mutation_calibration.confidence_effect == "supports_static_gap")
                && candidate.severity.as_deref() == Some("warning")
        }
    }
}

fn gate_reason(candidate: &GateCandidate, context: GateReasonContext<'_>) -> String {
    if candidate.suppressed || candidate.configured_off {
        return format!(
            "configured-hidden or suppressed candidate preserved as `{}`",
            candidate
                .suppression_reason
                .as_deref()
                .unwrap_or("suppressed")
        );
    }
    if !context.eligible {
        if candidate.source == "gap_decision_ledger" {
            if !candidate.gap_ledger_gate_candidate {
                return format!(
                    "gap decision ledger record is not gate-candidate eligible: {}",
                    candidate
                        .gap_ledger_gate_reason
                        .as_deref()
                        .unwrap_or("not_gate_candidate")
                );
            }
            if !candidate.gap_ledger_safe_gate_predicate {
                return "gap decision ledger record does not satisfy the safe gate predicate"
                    .to_string();
            }
            if candidate.repair_route.is_none() || candidate.verification_commands.is_empty() {
                return "gap decision ledger record is missing repair route or verification command"
                    .to_string();
            }
            if candidate.placement.path.is_none() || candidate.placement.line.is_none() {
                return "gap decision ledger record is missing a stable file and line anchor"
                    .to_string();
            }
        }
        if candidate.source == "summary_only" {
            return "summary-only recommendation remains visible and advisory".to_string();
        }
        if candidate.nearby_test_changed {
            return "nearby focused test changed in this PR, so the candidate stays advisory"
                .to_string();
        }
        if !has_concrete_guidance(candidate) {
            return "candidate is missing concrete focused-test guidance".to_string();
        }
        return "candidate is outside the initial policy-eligible class or placement scope"
            .to_string();
    }
    match context.decision {
        "acknowledged" => format!(
            "policy-eligible gap acknowledged by {}",
            context
                .acknowledgement_label
                .unwrap_or(DEFAULT_ACKNOWLEDGEMENT_LABEL)
        ),
        "blocking"
            if candidate.source == "gap_decision_ledger"
                && context.mode == GateMode::BaselineCheck
                && context.is_baseline_new =>
        {
            format!(
                "new repairable {} gap blocks under baseline-check from gap decision ledger",
                candidate.gap_kind.as_deref().unwrap_or("Rust")
            )
        }
        "blocking" if context.mode == GateMode::BaselineCheck && context.is_baseline_new => {
            "new policy-eligible gap blocks under baseline-check".to_string()
        }
        "blocking" if candidate.source == "gap_decision_ledger" => format!(
            "new repairable {} gap blocks from gap decision ledger",
            candidate.gap_kind.as_deref().unwrap_or("Rust")
        ),
        "blocking" if context.mode == GateMode::CalibratedGate => {
            if context.mutation_calibration.confidence_effect == "supports_static_gap" {
                "new policy-eligible gap has supporting imported mutation calibration".to_string()
            } else {
                "new policy-eligible gap has supporting recommendation calibration".to_string()
            }
        }
        "blocking" => "policy-eligible gap blocks under acknowledgeable mode".to_string(),
        _ if context.mode == GateMode::VisibleOnly => {
            "visible-only mode records evidence without blocking".to_string()
        }
        _ if !context.is_baseline_new => {
            "candidate identity is already present in the explicit baseline".to_string()
        }
        _ if context.recommendation_calibration.available
            && context.recommendation_calibration.confidence_effect == "keeps_advisory" =>
        {
            "recommendation calibration keeps this candidate advisory".to_string()
        }
        _ if context.mutation_calibration.available
            && context.mutation_calibration.confidence_effect == "keeps_advisory" =>
        {
            "imported mutation calibration keeps this candidate advisory".to_string()
        }
        _ => "candidate remains advisory under current policy inputs".to_string(),
    }
}

fn summarize_decisions(decisions: &[GateDecision]) -> GateSummary {
    let mut summary = GateSummary {
        evaluated: decisions.len(),
        ..GateSummary::default()
    };
    for decision in decisions {
        match decision.decision.as_str() {
            "blocking" => summary.blocking += 1,
            "acknowledged" => summary.acknowledged += 1,
            "advisory" => summary.advisory += 1,
            "suppressed" => summary.suppressed += 1,
            "not_applicable" => summary.not_applicable += 1,
            _ => {}
        }
        if decision.decision == "advisory"
            && decision
                .evidence
                .recommendation_calibration
                .confidence_effect
                == "not_used"
            && candidate_class_is_policy_eligible(decision.static_class.as_deref())
        {
            summary.unknown_confidence += 1;
        }
    }
    summary
}

fn top_level_status(
    summary: &GateSummary,
    warnings: &[String],
    config_errors: &[String],
    mode: GateMode,
) -> &'static str {
    if !config_errors.is_empty() {
        "config_error"
    } else if summary.blocking > 0 {
        "blocked"
    } else if summary.acknowledged > 0 {
        "acknowledged"
    } else if mode == GateMode::VisibleOnly
        || summary.advisory > 0
        || summary.suppressed > 0
        || summary.unknown_confidence > 0
        || !warnings.is_empty()
    {
        "advisory"
    } else {
        "pass"
    }
}

fn acknowledgement_labels(input: &GateEvaluateInput) -> Vec<String> {
    if input.acknowledgement_labels.is_empty() {
        vec![DEFAULT_ACKNOWLEDGEMENT_LABEL.to_string()]
    } else {
        input.acknowledgement_labels.clone()
    }
}

fn read_json_value_with_display(path: &Path, display: &Path) -> Result<Value, String> {
    let display = display_path(display);
    let text = fs::read_to_string(path).map_err(|err| {
        if err.kind() == std::io::ErrorKind::NotFound {
            format!("read {display} failed: not found")
        } else {
            format!("read {display} failed: {err}")
        }
    })?;
    serde_json::from_str(&text).map_err(|err| format!("parse {display} failed: {err}"))
}

fn string_field(value: Option<&Value>) -> Option<String> {
    value
        .and_then(Value::as_str)
        .filter(|text| !text.is_empty())
        .map(ToOwned::to_owned)
}

fn non_empty_string(value: &str) -> Option<String> {
    (!value.trim().is_empty()).then(|| value.to_string())
}

fn canonical_gap_id_from_value(value: &Value) -> Option<String> {
    string_field(value.get("canonical_gap_id"))
        .or_else(|| string_field(value.pointer("/identity/canonical_gap_id")))
        .or_else(|| string_field(value.pointer("/evidence_record/canonical_gap_id")))
}

fn resolve_root_path(root: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        root.join(path)
    }
}

fn display_path(path: &Path) -> String {
    let value = path.display().to_string().replace('\\', "/");
    if value.is_empty() {
        ".".to_string()
    } else {
        value.strip_prefix("./").unwrap_or(&value).to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn gate_visible_only_records_pr_guidance_without_blocking() -> Result<(), String> {
        let input = fixture_input(GateMode::VisibleOnly);
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "advisory");
        assert_eq!(report.summary.evaluated, 1);
        assert_eq!(report.summary.advisory, 1);
        assert!(!gate_decision_should_fail(&report));
        let json_text = render_gate_decision_json(&report)?;
        let value: Value = serde_json::from_str(&json_text)
            .map_err(|err| format!("gate decision JSON should parse: {err}"))?;
        assert_eq!(value["schema_version"], SCHEMA_VERSION);
        assert_eq!(value["status"], "advisory");
        assert_eq!(value["decisions"][0]["decision"], "advisory");
        assert_eq!(
            value["decisions"][0]["evidence"]["recommended_test"],
            "tests/pricing.rs::above_threshold_gets_discount"
        );
        let markdown = render_gate_decision_markdown(&report);
        assert!(markdown.contains("# RIPR Gate Decision"));
        assert!(markdown.contains("Decision: advisory"));
        assert!(markdown.contains("visible-only mode records evidence without blocking"));
        Ok(())
    }

    #[test]
    fn gate_acknowledgeable_blocks_policy_candidate_without_label() -> Result<(), String> {
        let input = fixture_input(GateMode::Acknowledgeable);
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "blocked");
        assert_eq!(report.summary.blocking, 1);
        assert!(gate_decision_should_fail(&report));
        assert_eq!(report.decisions[0].decision, "blocking");
        Ok(())
    }

    #[test]
    fn gate_acknowledgeable_keeps_waived_candidate_visible() -> Result<(), String> {
        let mut input = fixture_input(GateMode::Acknowledgeable);
        input.labels.push("ripr-waive".to_string());
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "acknowledged");
        assert_eq!(report.summary.acknowledged, 1);
        assert!(!gate_decision_should_fail(&report));
        assert_eq!(
            report.decisions[0].policy.acknowledgement_label,
            Some("ripr-waive".to_string())
        );
        Ok(())
    }

    #[test]
    fn gate_calibrated_mode_requires_explicit_baseline() -> Result<(), String> {
        let input = fixture_input(GateMode::CalibratedGate);
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "config_error");
        assert_eq!(report.summary.evaluated, 0);
        assert!(gate_decision_should_fail(&report));
        assert!(
            report
                .config_errors
                .iter()
                .any(|error| error.contains("requires an explicit --baseline"))
        );
        Ok(())
    }

    #[test]
    fn gate_calibrated_mode_blocks_new_supported_candidate() -> Result<(), String> {
        let dir = temp_dir("gate-calibrated")?;
        let baseline = dir.join("baseline.json");
        fs::write(&baseline, r#"{"schema_version":"0.1","decisions":[]}"#)
            .map_err(|err| format!("write baseline failed: {err}"))?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.recommendation_calibration = Some(PathBuf::from(
            "fixtures/boundary_gap/expected/recommendation-calibration/recommendation-calibration.json",
        ));
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "blocked");
        assert_eq!(report.summary.blocking, 1);
        assert_eq!(
            report.decisions[0]
                .evidence
                .recommendation_calibration
                .confidence_effect,
            "supports_static_gap"
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_calibrated_mode_uses_imported_mutation_support() -> Result<(), String> {
        let dir = temp_dir("gate-mutation-calibrated")?;
        let baseline = dir.join("baseline.json");
        let mutation = dir.join("mutation-calibration.json");
        fs::write(&baseline, r#"{"schema_version":"0.1","decisions":[]}"#)
            .map_err(|err| format!("write baseline failed: {err}"))?;
        fs::write(
            &mutation,
            r#"{
              "schema_version": "0.1",
              "matches": [
                {
                  "join_method": "seam_id",
                  "runtime": {
                    "seam_id": "8f7fa8644fd12280",
                    "runtime_outcome": "missed"
                  },
                  "static": {
                    "seam_id": "8f7fa8644fd12280"
                  }
                }
              ],
              "ambiguous_file_line_matches": []
            }"#,
        )
        .map_err(|err| format!("write mutation calibration failed: {err}"))?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.mutation_calibration = Some(mutation);
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "blocked");
        assert_eq!(report.summary.blocking, 1);
        assert_eq!(
            report.decisions[0]
                .evidence
                .mutation_calibration
                .confidence_effect,
            "supports_static_gap"
        );
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("imported mutation calibration")
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_labels_json_acknowledges_candidate() -> Result<(), String> {
        let dir = temp_dir("gate-labels-json")?;
        let labels = dir.join("labels.json");
        fs::write(&labels, r#"{"labels":["ripr-waive"]}"#)
            .map_err(|err| format!("write labels failed: {err}"))?;
        let mut input = fixture_input(GateMode::Acknowledgeable);
        input.labels_json = Some(labels);
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "acknowledged");
        assert_eq!(report.inputs.labels, vec!["ripr-waive".to_string()]);
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_baseline_check_keeps_existing_candidate_advisory() -> Result<(), String> {
        let dir = temp_dir("gate-baseline-existing")?;
        let baseline = dir.join("baseline.json");
        fs::write(
            &baseline,
            r#"{
              "schema_version": "0.1",
              "decisions": [
                {"seam_id": "8f7fa8644fd12280", "source_id": "ripr-review-8f7fa8644fd12280"}
              ]
            }"#,
        )
        .map_err(|err| format!("write baseline failed: {err}"))?;
        let mut input = fixture_input(GateMode::BaselineCheck);
        input.baseline = Some(baseline);
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "advisory");
        assert_eq!(report.summary.blocking, 0);
        assert_eq!(report.summary.advisory, 1);
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("explicit baseline")
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_baseline_check_reads_baseline_ledger_entries() -> Result<(), String> {
        let dir = temp_dir("gate-baseline-ledger-entry")?;
        let baseline = dir.join("baseline.json");
        fs::write(
            &baseline,
            r#"{
              "schema_version": "0.1",
              "kind": "gate_baseline",
              "entries": [
                {
                  "identity": {
                    "seam_id": "8f7fa8644fd12280",
                    "source_id": "ripr-review-8f7fa8644fd12280",
                    "id": "ripr-gate-8f7fa8644fd12280",
                    "dedupe_key": null,
                    "fallback": "src/pricing.rs:88:weakly_gripped"
                  }
                }
              ]
            }"#,
        )
        .map_err(|err| format!("write baseline failed: {err}"))?;
        let mut input = fixture_input(GateMode::BaselineCheck);
        input.baseline = Some(baseline);
        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "advisory");
        assert_eq!(report.summary.blocking, 0);
        assert_eq!(report.summary.advisory, 1);
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("explicit baseline")
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_baseline_check_matches_canonical_gap_id_from_evidence_record() -> Result<(), String> {
        let dir = temp_dir("gate-baseline-canonical")?;
        let baseline = write_temp_json(
            &dir,
            "baseline.json",
            r#"{
              "schema_version": "0.1",
              "kind": "gate_baseline",
              "entries": [
                {
                  "identity": {
                    "canonical_gap_id": "pricing::discount::threshold_equality",
                    "seam_id": "old-seam",
                    "source_id": "old-review-id",
                    "fallback": "src/pricing.rs:88:weakly_gripped"
                  }
                }
              ]
            }"#,
        )?;
        let guidance = write_temp_json(
            &dir,
            "comments.json",
            r#"{
              "schema_version": "0.1",
              "summary": {"unchanged_tests": true},
              "comments": [
                {
                  "id": "ripr-review-new-line",
                  "seam_id": "new-seam",
                  "grip_class": "weakly_gripped",
                  "severity": "warning",
                  "missing_discriminator": "amount == discount_threshold",
                  "placement": {"path": "src/pricing.rs", "line": 144},
                  "suggested_test": {
                    "candidate_values": ["amount == discount_threshold"],
                    "near_test": "above_threshold_gets_discount"
                  },
                  "evidence_record": {
                    "canonical_gap_id": "pricing::discount::threshold_equality"
                  }
                }
              ],
              "summary_only": [],
              "suppressed": []
            }"#,
        )?;
        let mut input = fixture_input(GateMode::BaselineCheck);
        input.pr_guidance = Some(guidance);
        input.baseline = Some(baseline);

        let report = build_gate_decision_report(&input)?;
        let rendered = render_gate_decision_json(&report)?;
        let value: Value = serde_json::from_str(&rendered)
            .map_err(|err| format!("gate decision JSON should parse: {err}"))?;

        assert_eq!(report.status, "advisory");
        assert_eq!(report.summary.blocking, 0);
        assert_eq!(report.summary.advisory, 1);
        assert_eq!(
            report.decisions[0].policy.baseline_identity.as_deref(),
            Some("pricing::discount::threshold_equality")
        );
        assert_eq!(
            value["decisions"][0]["canonical_gap_id"],
            "pricing::discount::threshold_equality"
        );
        assert_eq!(
            value["decisions"][0]["policy"]["baseline_identity"],
            "pricing::discount::threshold_equality"
        );
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("explicit baseline")
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_baseline_index_reads_all_canonical_gap_identity_shapes() {
        let value = json!({
          "entries": [
            {"canonical_gap_id": "gap:direct"},
            {"identity": {"canonical_gap_id": "gap:identity"}},
            {"evidence_record": {"canonical_gap_id": "gap:record"}}
          ],
          "decisions": [
            {"canonical_gap_id": "gap:decision-direct"},
            {"identity": {"canonical_gap_id": "gap:decision-identity"}},
            {"evidence_record": {"canonical_gap_id": "gap:decision-record"}}
          ],
          "comments": [
            {"canonical_gap_id": "gap:comment-direct"},
            {"identity": {"canonical_gap_id": "gap:comment-identity"}},
            {"evidence_record": {"canonical_gap_id": "gap:comment-record"}}
          ],
          "summary_only": [
            {"canonical_gap_id": "gap:summary-direct"}
          ],
          "suppressed": [
            {"canonical_gap_id": "gap:suppressed-direct"}
          ]
        });
        let index = baseline_index_from_value(&value);

        for expected in [
            "gap:direct",
            "gap:identity",
            "gap:record",
            "gap:decision-direct",
            "gap:decision-identity",
            "gap:decision-record",
            "gap:comment-direct",
            "gap:comment-identity",
            "gap:comment-record",
            "gap:summary-direct",
            "gap:suppressed-direct",
        ] {
            assert!(
                index.identities.contains(expected),
                "expected baseline identity {expected}"
            );
        }
    }

    #[test]
    fn gate_candidate_reads_canonical_gap_id_from_supported_shapes() {
        for (value, expected) in [
            (json!({"canonical_gap_id": "gap:direct"}), "gap:direct"),
            (
                json!({"identity": {"canonical_gap_id": "gap:identity"}}),
                "gap:identity",
            ),
            (
                json!({"evidence_record": {"canonical_gap_id": "gap:record"}}),
                "gap:record",
            ),
        ] {
            assert_eq!(
                canonical_gap_id_from_value(&value).as_deref(),
                Some(expected)
            );
        }

        assert_eq!(canonical_gap_id_from_value(&json!({})), None);
    }

    #[test]
    fn gate_mode_parse_covers_all_values_and_unknowns() {
        assert_eq!(GateMode::parse("visible-only"), Ok(GateMode::VisibleOnly));
        assert_eq!(
            GateMode::parse("acknowledgeable"),
            Ok(GateMode::Acknowledgeable)
        );
        assert_eq!(
            GateMode::parse("baseline-check"),
            Ok(GateMode::BaselineCheck)
        );
        assert_eq!(
            GateMode::parse("calibrated-gate"),
            Ok(GateMode::CalibratedGate)
        );
        assert_eq!(
            GateMode::parse("hard"),
            Err("unknown gate mode `hard`".to_string())
        );
    }

    #[test]
    fn gate_optional_inputs_emit_warnings_and_markdown_sections() -> Result<(), String> {
        let dir = temp_dir("gate-optional-warnings")?;
        let invalid = write_temp_json(&dir, "invalid.json", "{")?;
        let mut input = fixture_input(GateMode::VisibleOnly);
        input.root = dir.clone();
        input.pr_guidance = Some(write_temp_json(&dir, "comments.json", PR_GUIDANCE_JSON)?);
        input.repo_exposure = Some(PathBuf::from("missing-repo.json"));
        input.sarif_policy = Some(
            invalid
                .strip_prefix(&dir)
                .map_err(|err| err.to_string())?
                .to_path_buf(),
        );
        input.labels_json = Some(input.sarif_policy.clone().unwrap_or_default());
        input.agent_verify = Some(PathBuf::from("missing-verify.json"));
        input.agent_receipt = Some(input.sarif_policy.clone().unwrap_or_default());
        input.recommendation_calibration = Some(PathBuf::from("missing-recommendation.json"));
        input.mutation_calibration = Some(input.sarif_policy.clone().unwrap_or_default());
        input.baseline = Some(input.sarif_policy.clone().unwrap_or_default());

        let report = build_gate_decision_report(&input)?;
        let mut warning_report = report.clone();
        warning_report
            .warnings
            .push("manual | warning\nwith newline".to_string());
        let markdown = render_gate_decision_markdown(&warning_report);

        assert_eq!(report.status, "advisory");
        assert!(
            report
                .warnings
                .iter()
                .any(|warning| warning.contains("optional repo_exposure"))
        );
        assert!(
            report
                .warnings
                .iter()
                .any(|warning| warning.contains("optional labels_json"))
        );
        assert!(markdown.contains("## Warnings"));
        assert!(markdown.contains("manual \\| warning with newline"));
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_config_errors_render_markdown_and_fail_status() -> Result<(), String> {
        let input = GateEvaluateInput {
            root: repo_root(),
            repo_exposure: None,
            pr_guidance: Some(PathBuf::from("missing-comments.json")),
            gap_ledger: None,
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::BaselineCheck,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        let markdown = render_gate_decision_markdown(&report);

        assert_eq!(report.status, "config_error");
        assert!(gate_decision_should_fail(&report));
        assert!(markdown.contains("## Config Errors"));
        assert!(markdown.contains("requires an explicit --baseline"));
        Ok(())
    }

    #[test]
    fn gate_summary_only_and_suppressed_candidates_remain_visible() -> Result<(), String> {
        let dir = temp_dir("gate-summary-suppressed")?;
        let guidance = write_temp_json(&dir, "comments.json", SUMMARY_AND_SUPPRESSED_JSON)?;
        let mut input = fixture_input(GateMode::Acknowledgeable);
        input.root = dir.clone();
        input.pr_guidance = Some(
            guidance
                .strip_prefix(&dir)
                .map_err(|err| err.to_string())?
                .to_path_buf(),
        );

        let report = build_gate_decision_report(&input)?;

        assert_eq!(report.status, "advisory");
        assert_eq!(report.summary.suppressed, 1);
        assert_eq!(report.summary.advisory, 1);
        assert!(
            report
                .decisions
                .iter()
                .any(|decision| decision.gate_reason.contains("summary-only"))
        );
        assert!(
            report
                .decisions
                .iter()
                .any(|decision| decision.gate_reason.contains("configured-hidden"))
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_changed_test_and_missing_guidance_candidates_stay_advisory() -> Result<(), String> {
        let dir = temp_dir("gate-ineligible")?;
        let guidance = write_temp_json(&dir, "comments.json", INELIGIBLE_GUIDANCE_JSON)?;
        let mut input = fixture_input(GateMode::Acknowledgeable);
        input.root = dir.clone();
        input.pr_guidance = Some(
            guidance
                .strip_prefix(&dir)
                .map_err(|err| err.to_string())?
                .to_path_buf(),
        );

        let report = build_gate_decision_report(&input)?;

        assert_eq!(report.status, "advisory");
        assert_eq!(report.summary.blocking, 0);
        assert!(
            report
                .decisions
                .iter()
                .any(|decision| decision.gate_reason.contains("nearby focused test changed"))
        );
        let missing_guidance = write_temp_json(&dir, "missing.json", MISSING_GUIDANCE_JSON)?;
        input.pr_guidance = Some(
            missing_guidance
                .strip_prefix(&dir)
                .map_err(|err| err.to_string())?
                .to_path_buf(),
        );
        let report = build_gate_decision_report(&input)?;
        assert!(
            report
                .decisions
                .iter()
                .any(|decision| decision.gate_reason.contains("missing concrete"))
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_baseline_check_blocks_new_candidate() -> Result<(), String> {
        let dir = temp_dir("gate-baseline-new")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let mut input = fixture_input(GateMode::BaselineCheck);
        input.baseline = Some(baseline);

        let report = build_gate_decision_report(&input)?;

        assert_eq!(report.status, "blocked");
        assert_eq!(report.summary.blocking, 1);
        assert!(report.decisions[0].gate_reason.contains("baseline-check"));
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_acknowledgeable_blocks_safe_gap_ledger_candidate() -> Result<(), String> {
        let dir = temp_dir("gate-gap-ledger-block")?;
        let gap_ledger = write_temp_json(&dir, "gap-ledger.json", GAP_LEDGER_BLOCKING_JSON)?;
        let input = GateEvaluateInput {
            root: dir.clone(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: Some(
                gap_ledger
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::Acknowledgeable,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        let rendered = render_gate_decision_json(&report)?;
        let value: Value = serde_json::from_str(&rendered)
            .map_err(|err| format!("gate decision JSON should parse: {err}"))?;

        assert_eq!(report.status, "blocked");
        assert_eq!(report.summary.blocking, 1);
        assert_eq!(report.decisions[0].source, "gap_decision_ledger");
        assert_eq!(report.decisions[0].gap_id.as_deref(), Some("gap:pricing"));
        assert_eq!(
            value["inputs"]["gap_ledger"], "gap-ledger.json",
            "gate report should name the explicit gap ledger input"
        );
        assert_eq!(
            value["decisions"][0]["gap_kind"],
            "MissingBoundaryAssertion"
        );
        assert_eq!(
            value["decisions"][0]["evidence"]["repair_route"]["route_kind"],
            "AddBoundaryAssertion"
        );
        assert_eq!(
            value["decisions"][0]["evidence"]["verification_commands"][0],
            "cargo xtask fixtures boundary_gap"
        );
        assert_eq!(
            value["decisions"][0]["evidence"]["candidate_values"],
            Value::Array(Vec::new()),
            "gap ledger records do not carry test input variants"
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_gap_ledger_static_unknown_only_stays_report_only() -> Result<(), String> {
        let dir = temp_dir("gate-gap-ledger-report-only")?;
        let gap_ledger = write_temp_json(&dir, "gap-ledger.json", GAP_LEDGER_REPORT_ONLY_JSON)?;
        let input = GateEvaluateInput {
            root: dir.clone(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: Some(
                gap_ledger
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::Acknowledgeable,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;

        assert_eq!(report.status, "pass");
        assert_eq!(report.summary.blocking, 0);
        assert_eq!(report.summary.not_applicable, 1);
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("not gate-candidate eligible")
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_labels_array_supports_custom_acknowledgement_label() -> Result<(), String> {
        let dir = temp_dir("gate-label-array")?;
        let labels = write_temp_json(&dir, "labels.json", r#"["accepted-risk"]"#)?;
        let mut input = fixture_input(GateMode::Acknowledgeable);
        input.labels_json = Some(labels);
        input.acknowledgement_labels = vec!["accepted-risk".to_string()];

        let report = build_gate_decision_report(&input)?;

        assert_eq!(report.status, "acknowledged");
        assert_eq!(
            report.decisions[0].policy.acknowledgement_label.as_deref(),
            Some("accepted-risk")
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn gate_calibration_can_keep_candidates_advisory() -> Result<(), String> {
        let dir = temp_dir("gate-calibration-advisory")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let recommendation = write_temp_json(
            &dir,
            "recommendation.json",
            r#"{"recommendations":[{"id":"ripr-review-8f7fa8644fd12280","calibration":{"outcome":"wrong_target"}}]}"#,
        )?;
        let mutation = write_temp_json(
            &dir,
            "mutation.json",
            r#"{
              "matches": [
                {
                  "static": {"seam_id": "other-seam"},
                  "runtime": {"runtime_outcome": "caught"}
                }
              ],
              "static_only_findings": [
                {"static": {"seam_id": "8f7fa8644fd12280"}}
              ],
              "ambiguous_file_line_matches": [{"file":"src/lib.rs","line":7}]
            }"#,
        )?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.recommendation_calibration = Some(recommendation);
        input.mutation_calibration = Some(mutation);

        let report = build_gate_decision_report(&input)?;

        assert_eq!(report.status, "advisory");
        assert_eq!(
            report.decisions[0]
                .evidence
                .recommendation_calibration
                .confidence_effect,
            "keeps_advisory"
        );
        assert!(
            report
                .warnings
                .iter()
                .any(|warning| warning.contains("ambiguous file/line"))
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn calibrated_gate_fixture_matrix_matches_checked_outputs() -> Result<(), String> {
        let cases = [
            GateFixtureCase {
                name: "visible-only-advisory",
                mode: GateMode::VisibleOnly,
                pr_guidance: "fixtures/boundary_gap/expected/pr-guidance/exact-line/comments.json",
                labels_json: None,
                labels: &[],
                recommendation_calibration: None,
                mutation_calibration: None,
                baseline: None,
            },
            GateFixtureCase {
                name: "acknowledged-waiver",
                mode: GateMode::Acknowledgeable,
                pr_guidance: "fixtures/boundary_gap/expected/pr-guidance/exact-line/comments.json",
                labels_json: Some(
                    "fixtures/boundary_gap/expected/calibrated-gate/acknowledged-waiver/labels.json",
                ),
                labels: &["ripr-waive"],
                recommendation_calibration: None,
                mutation_calibration: None,
                baseline: None,
            },
            GateFixtureCase {
                name: "baseline-check-existing",
                mode: GateMode::BaselineCheck,
                pr_guidance: "fixtures/boundary_gap/expected/pr-guidance/exact-line/comments.json",
                labels_json: None,
                labels: &[],
                recommendation_calibration: None,
                mutation_calibration: None,
                baseline: Some(
                    "fixtures/boundary_gap/expected/calibrated-gate/baseline-check-existing/baseline.json",
                ),
            },
            GateFixtureCase {
                name: "calibrated-high-confidence-new-gap",
                mode: GateMode::CalibratedGate,
                pr_guidance: "fixtures/boundary_gap/expected/pr-guidance/exact-line/comments.json",
                labels_json: None,
                labels: &[],
                recommendation_calibration: Some(
                    "fixtures/boundary_gap/expected/recommendation-calibration/recommendation-calibration.json",
                ),
                mutation_calibration: None,
                baseline: Some(
                    "fixtures/boundary_gap/expected/calibrated-gate/calibrated-high-confidence-new-gap/baseline.json",
                ),
            },
            GateFixtureCase {
                name: "summary-and-suppressed",
                mode: GateMode::Acknowledgeable,
                pr_guidance: "fixtures/boundary_gap/expected/calibrated-gate/summary-and-suppressed/pr-guidance.json",
                labels_json: None,
                labels: &[],
                recommendation_calibration: None,
                mutation_calibration: None,
                baseline: None,
            },
            GateFixtureCase {
                name: "missing-input",
                mode: GateMode::BaselineCheck,
                pr_guidance: "fixtures/boundary_gap/expected/calibrated-gate/missing-input/missing-comments.json",
                labels_json: None,
                labels: &[],
                recommendation_calibration: None,
                mutation_calibration: None,
                baseline: Some(
                    "fixtures/boundary_gap/expected/calibrated-gate/baseline-check-existing/baseline.json",
                ),
            },
            GateFixtureCase {
                name: "calibration-disagreement",
                mode: GateMode::CalibratedGate,
                pr_guidance: "fixtures/boundary_gap/expected/pr-guidance/exact-line/comments.json",
                labels_json: None,
                labels: &[],
                recommendation_calibration: Some(
                    "fixtures/boundary_gap/expected/calibrated-gate/calibration-disagreement/recommendation-calibration.json",
                ),
                mutation_calibration: Some(
                    "fixtures/boundary_gap/expected/calibrated-gate/calibration-disagreement/mutation-calibration.json",
                ),
                baseline: Some(
                    "fixtures/boundary_gap/expected/calibrated-gate/calibration-disagreement/baseline.json",
                ),
            },
        ];

        for case in cases {
            let input = case.input();
            let mut report = build_gate_decision_report(&input)?;
            report.root = ".".to_string();
            let rendered_json = render_gate_decision_json(&report)?;
            let rendered_md = render_gate_decision_markdown(&report);
            let expected_dir =
                PathBuf::from("fixtures/boundary_gap/expected/calibrated-gate").join(case.name);
            let expected_json = read_repo_fixture(&expected_dir.join("gate-decision.json"))?;
            let expected_md = read_repo_fixture(&expected_dir.join("gate-decision.md"))?;

            assert_eq!(rendered_json, expected_json, "{} JSON drifted", case.name);
            assert_eq!(rendered_md, expected_md, "{} Markdown drifted", case.name);
        }

        Ok(())
    }

    #[test]
    fn display_path_normalizes_empty_and_dot_prefixed_paths() {
        assert_eq!(display_path(Path::new("")), ".");
        assert_eq!(
            display_path(Path::new("./target/out.json")),
            "target/out.json"
        );
    }

    // -- coverage-gap tests --

    #[test]
    fn given_both_pr_guidance_and_gap_ledger_missing_when_evaluated_then_config_error()
    -> Result<(), String> {
        let input = GateEvaluateInput {
            root: repo_root(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: None,
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::VisibleOnly,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "config_error");
        assert!(gate_decision_should_fail(&report));
        assert!(
            report
                .config_errors
                .iter()
                .any(|error| error.contains("--pr-guidance") && error.contains("--gap-ledger")),
            "expected combined input requirement message, got {:?}",
            report.config_errors,
        );
        Ok(())
    }

    #[test]
    fn given_invalid_gap_ledger_json_when_evaluated_then_config_error_includes_parse_failure()
    -> Result<(), String> {
        let dir = temp_dir("gate-gap-ledger-invalid-json")?;
        let gap_ledger = write_temp_json(&dir, "gap-ledger.json", "{not valid json")?;
        let input = GateEvaluateInput {
            root: dir.clone(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: Some(
                gap_ledger
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::Acknowledgeable,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "config_error");
        assert!(
            report
                .config_errors
                .iter()
                .any(|error| error.contains("gap decision ledger")
                    && error.contains("is invalid")
                    && !error.contains("read failed")),
            "expected parse-failure config error, got {:?}",
            report.config_errors,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_unreadable_gap_ledger_when_evaluated_then_config_error_includes_read_failure()
    -> Result<(), String> {
        let dir = temp_dir("gate-gap-ledger-unreadable")?;
        let gap_ledger_dir = dir.join("gap-ledger.json");
        fs::create_dir_all(&gap_ledger_dir)
            .map_err(|err| format!("create gap-ledger dir failed: {err}"))?;
        let input = GateEvaluateInput {
            root: dir.clone(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: Some(
                gap_ledger_dir
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::Acknowledgeable,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "config_error");
        assert!(
            report
                .config_errors
                .iter()
                .any(|error| error.contains("gap decision ledger") && error.contains("read failed")),
            "expected read-failure config error, got {:?}",
            report.config_errors,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_unreadable_baseline_in_baseline_mode_then_config_error_includes_invalid_baseline()
    -> Result<(), String> {
        let dir = temp_dir("gate-baseline-unreadable")?;
        let baseline_dir = dir.join("baseline.json");
        fs::create_dir_all(&baseline_dir)
            .map_err(|err| format!("create baseline dir failed: {err}"))?;
        let mut input = fixture_input(GateMode::BaselineCheck);
        input.baseline = Some(baseline_dir);

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "config_error");
        assert!(
            report
                .config_errors
                .iter()
                .any(|error| error.contains("required baseline") && error.contains("is invalid")),
            "expected required-baseline-invalid config error, got {:?}",
            report.config_errors,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_recommendation_calibration_with_unknown_outcome_then_confidence_effect_is_unknown()
    -> Result<(), String> {
        let dir = temp_dir("gate-recommendation-unknown-outcome")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let recommendation = write_temp_json(
            &dir,
            "recommendation.json",
            r#"{
              "recommendations": [
                {
                  "id": "ripr-review-8f7fa8644fd12280",
                  "calibration": {"outcome": "novel-outcome"}
                }
              ]
            }"#,
        )?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.recommendation_calibration = Some(recommendation);

        let report = build_gate_decision_report(&input)?;
        assert_eq!(
            report.decisions[0]
                .evidence
                .recommendation_calibration
                .confidence_effect,
            "unknown"
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_mutation_calibration_with_unknown_outcome_then_confidence_effect_is_unknown()
    -> Result<(), String> {
        let dir = temp_dir("gate-mutation-unknown-outcome")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let mutation = write_temp_json(
            &dir,
            "mutation.json",
            r#"{
              "matches": [
                {
                  "static": {"seam_id": "8f7fa8644fd12280"},
                  "runtime": {"runtime_outcome": "novel-mutation-outcome"}
                }
              ]
            }"#,
        )?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.mutation_calibration = Some(mutation);

        let report = build_gate_decision_report(&input)?;
        assert_eq!(
            report.decisions[0]
                .evidence
                .mutation_calibration
                .confidence_effect,
            "unknown"
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_mutation_calibration_match_without_outcome_then_confidence_effect_is_not_used()
    -> Result<(), String> {
        let dir = temp_dir("gate-mutation-missing-outcome")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let mutation = write_temp_json(
            &dir,
            "mutation.json",
            r#"{
              "matches": [
                {
                  "static": {"seam_id": "8f7fa8644fd12280"},
                  "runtime": {}
                }
              ]
            }"#,
        )?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.mutation_calibration = Some(mutation);

        let report = build_gate_decision_report(&input)?;
        assert_eq!(
            report.decisions[0]
                .evidence
                .mutation_calibration
                .confidence_effect,
            "not_used"
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_mutation_calibration_match_without_seam_id_then_match_is_skipped() -> Result<(), String>
    {
        let dir = temp_dir("gate-mutation-no-seam-id")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let mutation = write_temp_json(
            &dir,
            "mutation.json",
            r#"{
              "matches": [
                {
                  "static": {},
                  "runtime": {"runtime_outcome": "missed"}
                }
              ]
            }"#,
        )?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.mutation_calibration = Some(mutation);

        let report = build_gate_decision_report(&input)?;
        assert_eq!(
            report.decisions[0]
                .evidence
                .mutation_calibration
                .confidence_effect,
            "not_used",
            "match without seam_id must not populate the mutation calibration index",
        );
        assert_eq!(report.status, "advisory");
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_guidance_with_recommended_file_only_then_recommended_test_is_file_path()
    -> Result<(), String> {
        let dir = temp_dir("gate-recommended-file-only")?;
        let guidance = write_temp_json(
            &dir,
            "comments.json",
            r#"{
              "schema_version": "0.1",
              "summary": {"unchanged_tests": true},
              "comments": [
                {
                  "id": "ripr-review-file-only",
                  "seam_id": "file-only-seam",
                  "grip_class": "weakly_gripped",
                  "severity": "warning",
                  "missing_discriminator": "amount == discount_threshold",
                  "placement": {"path": "src/pricing.rs", "line": 88},
                  "suggested_test": {
                    "recommended_file": "tests/pricing.rs",
                    "candidate_values": ["amount == discount_threshold"]
                  }
                }
              ],
              "summary_only": [],
              "suppressed": []
            }"#,
        )?;
        let mut input = fixture_input(GateMode::VisibleOnly);
        input.root = dir.clone();
        input.pr_guidance = Some(
            guidance
                .strip_prefix(&dir)
                .map_err(|err| err.to_string())?
                .to_path_buf(),
        );

        let report = build_gate_decision_report(&input)?;
        assert_eq!(
            report.decisions[0].evidence.recommended_test.as_deref(),
            Some("tests/pricing.rs"),
            "with no near_test the recommended file alone becomes the recommended test path",
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_candidate_without_any_identity_then_baseline_identity_uses_path_line_class_fallback() {
        let candidate = GateCandidate {
            source: "pr_guidance".to_string(),
            source_id: String::new(),
            gap_id: None,
            gap_kind: None,
            canonical_gap_id: None,
            seam_id: None,
            static_class: Some("weakly_gripped".to_string()),
            severity: Some("warning".to_string()),
            placement: GatePlacement {
                path: Some("src/pricing.rs".to_string()),
                line: Some(88),
            },
            missing_discriminator: None,
            assertion_shape: None,
            candidate_values: Vec::new(),
            recommended_test: None,
            repair_route: None,
            verification_commands: Vec::new(),
            nearby_test_changed: false,
            suppressed: false,
            configured_off: false,
            suppression_reason: None,
            gap_ledger_gate_candidate: false,
            gap_ledger_gate_reason: None,
            gap_ledger_safe_gate_predicate: false,
        };

        assert_eq!(
            baseline_identity(&candidate).as_deref(),
            Some("src/pricing.rs:88:weakly_gripped"),
            "fallback identity must encode file:line:class when no stable id exists",
        );

        let mut without_class = candidate.clone();
        without_class.static_class = None;
        assert_eq!(
            baseline_identity(&without_class).as_deref(),
            Some("src/pricing.rs:88:unknown"),
            "fallback identity tags missing class as `unknown`",
        );

        let mut without_placement = candidate;
        without_placement.placement = GatePlacement {
            path: None,
            line: None,
        };
        assert!(
            baseline_identity(&without_placement).is_none(),
            "without placement or id the fallback cannot synthesize an identity",
        );
    }

    #[test]
    fn given_calibrated_gate_with_mutation_keeps_advisory_then_gate_reason_cites_mutation_calibration()
    -> Result<(), String> {
        let dir = temp_dir("gate-mutation-keeps-advisory-reason")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let mutation = write_temp_json(
            &dir,
            "mutation.json",
            r#"{
              "matches": [
                {
                  "static": {"seam_id": "8f7fa8644fd12280"},
                  "runtime": {"runtime_outcome": "caught"}
                }
              ]
            }"#,
        )?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);
        input.mutation_calibration = Some(mutation);

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "advisory");
        assert_eq!(report.decisions[0].decision, "advisory");
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("imported mutation calibration keeps this candidate advisory"),
            "expected mutation-calibration advisory reason, got {:?}",
            report.decisions[0].gate_reason,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_calibrated_gate_without_any_calibration_then_gate_reason_falls_through_to_default_advisory()
    -> Result<(), String> {
        let dir = temp_dir("gate-calibrated-no-calibration-default")?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let mut input = fixture_input(GateMode::CalibratedGate);
        input.baseline = Some(baseline);

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "advisory");
        assert_eq!(report.decisions[0].decision, "advisory");
        assert_eq!(
            report.decisions[0].gate_reason,
            "candidate remains advisory under current policy inputs",
            "with neither calibration available the default advisory reason applies",
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_gap_ledger_record_with_eligible_projection_but_unsafe_predicate_then_reason_cites_predicate()
    -> Result<(), String> {
        let dir = temp_dir("gate-gap-ledger-unsafe-predicate")?;
        let gap_ledger = write_temp_json(
            &dir,
            "gap-ledger.json",
            r#"{
              "gap_records": [
                {
                  "gap_id": "gap:pricing",
                  "canonical_gap_id": "pricing::discount::unsafe",
                  "kind": "MissingBoundaryAssertion",
                  "language": "rust",
                  "language_status": "stable",
                  "scope": "pr_local",
                  "evidence_class": "weakly_exposed",
                  "gap_state": "actionable",
                  "policy_state": "new",
                  "repairability": "repairable",
                  "repair_route": {
                    "route_kind": "AddBoundaryAssertion",
                    "target_file": "tests/pricing.rs",
                    "related_test": "tests/pricing.rs::above_threshold_gets_discount",
                    "assertion_shape": "assert_eq!(price(threshold), discounted)",
                    "changed_behavior": "amount == discount_threshold"
                  },
                  "anchor": {
                    "file": "src/pricing.rs",
                    "line": 88,
                    "owner": "price",
                    "dedupe_fingerprint": "gap:pricing"
                  },
                  "projection_eligibility": {
                    "gate_candidate": {
                      "eligible": true,
                      "reason": "new_repairable_pr_local_gap"
                    }
                  },
                  "verification_commands": ["cargo xtask fixtures boundary_gap"],
                  "safe_gate_predicate": {
                    "policy_target_enabled": false,
                    "suppressed": false,
                    "waived": false,
                    "acknowledged_only": false,
                    "baseline_known": false,
                    "preview_language": false,
                    "static_unknown_only": false
                  }
                }
              ]
            }"#,
        )?;
        let input = GateEvaluateInput {
            root: dir.clone(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: Some(
                gap_ledger
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::Acknowledgeable,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.decisions[0].decision, "not_applicable");
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("safe gate predicate"),
            "expected safe-gate-predicate reason, got {:?}",
            report.decisions[0].gate_reason,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_gap_ledger_record_with_safe_predicate_but_missing_anchor_then_reason_cites_anchor()
    -> Result<(), String> {
        let dir = temp_dir("gate-gap-ledger-missing-anchor")?;
        let gap_ledger = write_temp_json(
            &dir,
            "gap-ledger.json",
            r#"{
              "gap_records": [
                {
                  "gap_id": "gap:pricing",
                  "canonical_gap_id": "pricing::discount::no_anchor",
                  "kind": "MissingBoundaryAssertion",
                  "language": "rust",
                  "language_status": "stable",
                  "scope": "pr_local",
                  "evidence_class": "weakly_exposed",
                  "gap_state": "actionable",
                  "policy_state": "new",
                  "repairability": "repairable",
                  "repair_route": {
                    "route_kind": "AddBoundaryAssertion",
                    "target_file": "tests/pricing.rs",
                    "related_test": "tests/pricing.rs::above_threshold_gets_discount",
                    "assertion_shape": "assert_eq!(price(threshold), discounted)",
                    "changed_behavior": "amount == discount_threshold"
                  },
                  "projection_eligibility": {
                    "gate_candidate": {
                      "eligible": true,
                      "reason": "new_repairable_pr_local_gap"
                    }
                  },
                  "verification_commands": ["cargo xtask fixtures boundary_gap"],
                  "safe_gate_predicate": {
                    "policy_target_enabled": true,
                    "suppressed": false,
                    "waived": false,
                    "acknowledged_only": false,
                    "baseline_known": false,
                    "preview_language": false,
                    "static_unknown_only": false
                  }
                }
              ]
            }"#,
        )?;
        let input = GateEvaluateInput {
            root: dir.clone(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: Some(
                gap_ledger
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode: GateMode::Acknowledgeable,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.decisions[0].decision, "not_applicable");
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("stable file and line anchor"),
            "expected missing-anchor reason, got {:?}",
            report.decisions[0].gate_reason,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_gap_ledger_record_in_baseline_check_with_new_identity_then_reason_cites_baseline_check_ledger()
    -> Result<(), String> {
        let dir = temp_dir("gate-gap-ledger-baseline-check-new")?;
        let gap_ledger = write_temp_json(&dir, "gap-ledger.json", GAP_LEDGER_BLOCKING_JSON)?;
        let baseline = write_temp_json(&dir, "baseline.json", r#"{"decisions":[]}"#)?;
        let input = GateEvaluateInput {
            root: dir.clone(),
            repo_exposure: None,
            pr_guidance: None,
            gap_ledger: Some(
                gap_ledger
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: Some(
                baseline
                    .strip_prefix(&dir)
                    .map_err(|err| err.to_string())?
                    .to_path_buf(),
            ),
            mode: GateMode::BaselineCheck,
            acknowledgement_labels: Vec::new(),
        };

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.status, "blocked");
        assert_eq!(report.decisions[0].decision, "blocking");
        assert_eq!(report.decisions[0].source, "gap_decision_ledger");
        assert!(
            report.decisions[0].gate_reason.contains("baseline-check")
                && report.decisions[0]
                    .gate_reason
                    .contains("gap decision ledger"),
            "expected baseline-check + gap ledger reason, got {:?}",
            report.decisions[0].gate_reason,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_class_not_policy_eligible_with_concrete_guidance_then_reason_cites_class_or_placement_scope()
    -> Result<(), String> {
        let dir = temp_dir("gate-class-not-policy-eligible")?;
        let guidance = write_temp_json(
            &dir,
            "comments.json",
            r#"{
              "schema_version": "0.1",
              "summary": {"unchanged_tests": true},
              "comments": [
                {
                  "id": "ripr-review-ungrippable",
                  "seam_id": "ungrippable-seam",
                  "grip_class": "off_seam",
                  "severity": "warning",
                  "missing_discriminator": "amount == discount_threshold",
                  "placement": {"path": "src/pricing.rs", "line": 88},
                  "suggested_test": {
                    "candidate_values": ["amount == discount_threshold"],
                    "near_test": "above_threshold_gets_discount"
                  }
                }
              ],
              "summary_only": [],
              "suppressed": []
            }"#,
        )?;
        let mut input = fixture_input(GateMode::Acknowledgeable);
        input.root = dir.clone();
        input.pr_guidance = Some(
            guidance
                .strip_prefix(&dir)
                .map_err(|err| err.to_string())?
                .to_path_buf(),
        );

        let report = build_gate_decision_report(&input)?;
        assert_eq!(report.decisions[0].decision, "advisory");
        assert!(
            report.decisions[0]
                .gate_reason
                .contains("policy-eligible class or placement scope"),
            "expected policy-eligible-class fallthrough reason, got {:?}",
            report.decisions[0].gate_reason,
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    #[test]
    fn given_read_json_value_pointed_at_directory_then_error_describes_non_not_found_failure()
    -> Result<(), String> {
        let dir = temp_dir("gate-read-json-dir")?;
        let target_dir = dir.join("not-a-file.json");
        fs::create_dir_all(&target_dir).map_err(|err| format!("create dir failed: {err}"))?;
        let display = PathBuf::from("not-a-file.json");

        let result = read_json_value_with_display(&target_dir, &display);

        let error = match result {
            Ok(_) => return Err("reading a directory must fail".to_string()),
            Err(error) => error,
        };
        assert!(
            error.starts_with("read not-a-file.json failed:") && !error.contains("not found"),
            "expected non-not-found read error, got {error}",
        );
        let _ = fs::remove_dir_all(dir);
        Ok(())
    }

    fn fixture_input(mode: GateMode) -> GateEvaluateInput {
        GateEvaluateInput {
            root: repo_root(),
            repo_exposure: None,
            pr_guidance: Some(PathBuf::from(
                "fixtures/boundary_gap/expected/pr-guidance/exact-line/comments.json",
            )),
            gap_ledger: None,
            sarif_policy: None,
            labels_json: None,
            labels: Vec::new(),
            agent_verify: None,
            agent_receipt: None,
            recommendation_calibration: None,
            mutation_calibration: None,
            baseline: None,
            mode,
            acknowledgement_labels: Vec::new(),
        }
    }

    struct GateFixtureCase {
        name: &'static str,
        mode: GateMode,
        pr_guidance: &'static str,
        labels_json: Option<&'static str>,
        labels: &'static [&'static str],
        recommendation_calibration: Option<&'static str>,
        mutation_calibration: Option<&'static str>,
        baseline: Option<&'static str>,
    }

    impl GateFixtureCase {
        fn input(&self) -> GateEvaluateInput {
            GateEvaluateInput {
                root: repo_root(),
                repo_exposure: None,
                pr_guidance: Some(PathBuf::from(self.pr_guidance)),
                gap_ledger: None,
                sarif_policy: None,
                labels_json: self.labels_json.map(PathBuf::from),
                labels: self
                    .labels
                    .iter()
                    .map(|label| (*label).to_string())
                    .collect(),
                agent_verify: None,
                agent_receipt: None,
                recommendation_calibration: self.recommendation_calibration.map(PathBuf::from),
                mutation_calibration: self.mutation_calibration.map(PathBuf::from),
                baseline: self.baseline.map(PathBuf::from),
                mode: self.mode,
                acknowledgement_labels: Vec::new(),
            }
        }
    }

    fn repo_root() -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .and_then(Path::parent)
            .map(Path::to_path_buf)
            .unwrap_or_else(|| PathBuf::from("."))
    }

    fn temp_dir(name: &str) -> Result<PathBuf, String> {
        let mut path = std::env::temp_dir();
        let stamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|err| format!("system time before unix epoch: {err}"))?
            .as_nanos();
        path.push(format!("ripr-{name}-{stamp}"));
        fs::create_dir_all(&path).map_err(|err| format!("create temp dir failed: {err}"))?;
        Ok(path)
    }

    fn write_temp_json(dir: &Path, name: &str, contents: &str) -> Result<PathBuf, String> {
        let path = dir.join(name);
        fs::write(&path, contents).map_err(|err| format!("write {name} failed: {err}"))?;
        Ok(path)
    }

    fn read_repo_fixture(path: &Path) -> Result<String, String> {
        let resolved = repo_root().join(path);
        fs::read_to_string(&resolved)
            .map_err(|err| format!("read {} failed: {err}", resolved.display()))
    }

    const PR_GUIDANCE_JSON: &str = r#"{
      "schema_version": "0.1",
      "summary": {"unchanged_tests": true},
      "comments": [
        {
          "id": "ripr-review-8f7fa8644fd12280",
          "seam_id": "8f7fa8644fd12280",
          "grip_class": "weakly_gripped",
          "severity": "warning",
          "missing_discriminator": "amount == discount_threshold",
          "placement": {"path": "src/pricing.rs", "line": 88},
          "suggested_test": {
            "candidate_values": ["amount == discount_threshold"],
            "near_test": "above_threshold_gets_discount"
          }
        }
      ],
      "summary_only": [],
      "suppressed": []
    }"#;

    const SUMMARY_AND_SUPPRESSED_JSON: &str = r#"{
      "schema_version": "0.1",
      "summary": {"unchanged_tests": true},
      "comments": [],
      "summary_only": [
        {
          "id": "summary-1",
          "seam_id": "summary-seam",
          "grip_class": "weakly_gripped",
          "severity": "warning",
          "missing_discriminator": "amount == discount_threshold",
          "placement": {"path": "src/pricing.rs", "line": 88}
        }
      ],
      "suppressed": [
        {
          "id": "suppressed-1",
          "seam_id": "suppressed-seam",
          "grip_class": "weakly_gripped",
          "severity": "off",
          "reason": "severity_off",
          "missing_discriminator": "amount == discount_threshold",
          "placement": {"path": "src/pricing.rs", "line": 89}
        }
      ]
    }"#;

    const INELIGIBLE_GUIDANCE_JSON: &str = r#"{
      "schema_version": "0.1",
      "summary": {"unchanged_tests": false},
      "comments": [
        {
          "id": "changed-test",
          "seam_id": "changed-test-seam",
          "grip_class": "weakly_gripped",
          "severity": "warning",
          "missing_discriminator": "amount == discount_threshold",
          "placement": {"path": "src/pricing.rs", "line": 88}
        },
        {
          "id": "missing-guidance",
          "seam_id": "missing-guidance-seam",
          "grip_class": "weakly_gripped",
          "severity": "warning",
          "placement": {"path": "src/pricing.rs", "line": 89}
        }
      ],
      "summary_only": [],
      "suppressed": []
    }"#;

    const MISSING_GUIDANCE_JSON: &str = r#"{
      "schema_version": "0.1",
      "summary": {"unchanged_tests": true},
      "comments": [
        {
          "id": "missing-guidance",
          "seam_id": "missing-guidance-seam",
          "grip_class": "weakly_gripped",
          "severity": "warning",
          "placement": {"path": "src/pricing.rs", "line": 89}
        }
      ],
      "summary_only": [],
      "suppressed": []
    }"#;

    const GAP_LEDGER_BLOCKING_JSON: &str = r#"{
      "gap_records": [
        {
          "gap_id": "gap:pricing",
          "canonical_gap_id": "pricing::discount::threshold",
          "kind": "MissingBoundaryAssertion",
          "language": "rust",
          "language_status": "stable",
          "scope": "pr_local",
          "evidence_class": "weakly_exposed",
          "gap_state": "actionable",
          "policy_state": "new",
          "repairability": "repairable",
          "repair_route": {
            "route_kind": "AddBoundaryAssertion",
            "target_file": "tests/pricing.rs",
            "related_test": "tests/pricing.rs::above_threshold_gets_discount",
            "assertion_shape": "assert_eq!(price(threshold), discounted)",
            "changed_behavior": "amount == discount_threshold"
          },
          "anchor": {
            "file": "src/pricing.rs",
            "line": 88,
            "owner": "price",
            "dedupe_fingerprint": "gap:pricing"
          },
          "projection_eligibility": {
            "gate_candidate": {
              "eligible": true,
              "reason": "new_repairable_pr_local_gap"
            }
          },
          "verification_commands": ["cargo xtask fixtures boundary_gap"],
          "safe_gate_predicate": {
            "policy_target_enabled": true,
            "suppressed": false,
            "waived": false,
            "acknowledged_only": false,
            "baseline_known": false,
            "preview_language": false,
            "static_unknown_only": false
          }
        }
      ]
    }"#;

    const GAP_LEDGER_REPORT_ONLY_JSON: &str = r#"{
      "gap_records": [
        {
          "gap_id": "gap:unknown",
          "canonical_gap_id": "pricing::unknown",
          "kind": "Unknown",
          "language": "rust",
          "language_status": "stable",
          "scope": "pr_local",
          "evidence_class": "static_unknown",
          "gap_state": "unknown",
          "policy_state": "new",
          "repairability": "analyzer_limitation",
          "anchor": {
            "file": "src/pricing.rs",
            "line": 90,
            "dedupe_fingerprint": "gap:unknown"
          },
          "projection_eligibility": {
            "gate_candidate": {
              "eligible": false,
              "reason": "static_unknown_only"
            }
          },
          "safe_gate_predicate": {
            "policy_target_enabled": true,
            "static_unknown_only": true
          }
        }
      ]
    }"#;
}
