use crate::data_model::aspa::AspaObject;
use crate::data_model::manifest::ManifestObject;
use crate::data_model::roa::RoaObject;
use crate::storage::{
    AuditRuleIndexEntry, AuditRuleKind, RawByHashEntry, RocksStore, ValidatedCaInstanceResult,
    VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirLocalOutput,
    VcirOutputType,
};
use serde::Serialize;
use std::collections::HashSet;

#[derive(Debug, thiserror::Error)]
pub enum AuditTraceError {
    #[error("storage error: {0}")]
    Storage(#[from] crate::storage::StorageError),

    #[error("audit rule index points to missing VCIR: {manifest_rsync_uri}")]
    MissingVcir { manifest_rsync_uri: String },

    #[error(
        "audit rule index points to missing local output: rule_hash={rule_hash}, output_id={output_id}, manifest={manifest_rsync_uri}"
    )]
    MissingLocalOutput {
        rule_hash: String,
        output_id: String,
        manifest_rsync_uri: String,
    },

    #[error("detected VCIR parent cycle at {manifest_rsync_uri}")]
    ParentCycle { manifest_rsync_uri: String },
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditTraceRawRef {
    pub sha256_hex: String,
    pub raw_present: bool,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub origin_uris: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub object_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub byte_len: Option<usize>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditTraceArtifact {
    pub artifact_role: VcirArtifactRole,
    pub artifact_kind: VcirArtifactKind,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub uri: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub object_type: Option<String>,
    pub validation_status: VcirArtifactValidationStatus,
    pub raw: AuditTraceRawRef,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditTraceChainNode {
    pub manifest_rsync_uri: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_manifest_rsync_uri: Option<String>,
    pub tal_id: String,
    pub ca_subject_name: String,
    pub ca_ski: String,
    pub issuer_ski: String,
    pub current_manifest_rsync_uri: String,
    pub current_crl_rsync_uri: String,
    pub last_successful_validation_time_rfc3339_utc: String,
    pub local_output_count: usize,
    pub child_count: usize,
    pub related_artifacts: Vec<AuditTraceArtifact>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditTraceResolvedOutput {
    pub output_id: String,
    pub output_type: VcirOutputType,
    pub rule_hash: String,
    pub source_object_uri: String,
    pub source_object_type: String,
    pub source_object_hash: String,
    pub source_ee_cert_hash: String,
    pub item_effective_until_rfc3339_utc: String,
    pub payload_json: String,
    pub validation_path_hint: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditRuleTrace {
    pub rule: AuditRuleIndexEntry,
    pub resolved_output: AuditTraceResolvedOutput,
    pub source_object_raw: AuditTraceRawRef,
    pub source_ee_cert_raw: AuditTraceRawRef,
    pub chain_leaf_to_root: Vec<AuditTraceChainNode>,
}

pub fn trace_rule_to_root(
    store: &RocksStore,
    kind: AuditRuleKind,
    rule_hash: &str,
) -> Result<Option<AuditRuleTrace>, AuditTraceError> {
    let Some(rule) = store.get_audit_rule_index_entry(kind, rule_hash)? else {
        return Ok(None);
    };

    let Some(leaf_vcir) = store.get_vcir(&rule.manifest_rsync_uri)? else {
        return Err(AuditTraceError::MissingVcir {
            manifest_rsync_uri: rule.manifest_rsync_uri.clone(),
        });
    };

    let Some(local_output) = leaf_vcir
        .local_outputs
        .iter()
        .find(|output| output.output_id == rule.output_id && output.rule_hash == rule.rule_hash)
        .or_else(|| {
            leaf_vcir
                .local_outputs
                .iter()
                .find(|output| output.rule_hash == rule.rule_hash)
        })
        .cloned()
    else {
        return Err(AuditTraceError::MissingLocalOutput {
            rule_hash: rule.rule_hash.clone(),
            output_id: rule.output_id.clone(),
            manifest_rsync_uri: rule.manifest_rsync_uri.clone(),
        });
    };

    let chain = trace_vcir_chain_to_root(store, &leaf_vcir.manifest_rsync_uri)?
        .expect("leaf VCIR already loaded must exist");

    Ok(Some(AuditRuleTrace {
        rule,
        resolved_output: resolved_output_from_local(&local_output),
        source_object_raw: resolve_raw_ref(
            store,
            &local_output.source_object_hash,
            Some(&local_output.source_object_uri),
            Some(local_output.source_object_type.as_str()),
        )?,
        source_ee_cert_raw: resolve_source_ee_cert_raw_ref(store, &local_output)?,
        chain_leaf_to_root: chain,
    }))
}

pub fn trace_vcir_chain_to_root(
    store: &RocksStore,
    manifest_rsync_uri: &str,
) -> Result<Option<Vec<AuditTraceChainNode>>, AuditTraceError> {
    let Some(mut current) = store.get_vcir(manifest_rsync_uri)? else {
        return Ok(None);
    };

    let mut seen = HashSet::new();
    let mut chain = Vec::new();
    loop {
        if !seen.insert(current.manifest_rsync_uri.clone()) {
            return Err(AuditTraceError::ParentCycle {
                manifest_rsync_uri: current.manifest_rsync_uri,
            });
        }
        let parent = current.parent_manifest_rsync_uri.clone();
        chain.push(trace_chain_node(store, &current)?);
        let Some(parent_manifest_rsync_uri) = parent else {
            break;
        };
        let Some(parent_vcir) = store.get_vcir(&parent_manifest_rsync_uri)? else {
            return Err(AuditTraceError::MissingVcir {
                manifest_rsync_uri: parent_manifest_rsync_uri,
            });
        };
        current = parent_vcir;
    }

    Ok(Some(chain))
}

fn trace_chain_node(
    store: &RocksStore,
    vcir: &ValidatedCaInstanceResult,
) -> Result<AuditTraceChainNode, AuditTraceError> {
    let mut related_artifacts = Vec::with_capacity(vcir.related_artifacts.len());
    for artifact in &vcir.related_artifacts {
        related_artifacts.push(AuditTraceArtifact {
            artifact_role: artifact.artifact_role,
            artifact_kind: artifact.artifact_kind,
            uri: artifact.uri.clone(),
            object_type: artifact.object_type.clone(),
            validation_status: artifact.validation_status,
            raw: resolve_raw_ref(
                store,
                &artifact.sha256,
                artifact.uri.as_deref(),
                artifact.object_type.as_deref(),
            )?,
        });
    }

    Ok(AuditTraceChainNode {
        manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
        parent_manifest_rsync_uri: vcir.parent_manifest_rsync_uri.clone(),
        tal_id: vcir.tal_id.clone(),
        ca_subject_name: vcir.ca_subject_name.clone(),
        ca_ski: vcir.ca_ski.clone(),
        issuer_ski: vcir.issuer_ski.clone(),
        current_manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
        current_crl_rsync_uri: vcir.current_crl_rsync_uri.clone(),
        last_successful_validation_time_rfc3339_utc: vcir
            .last_successful_validation_time
            .rfc3339_utc
            .clone(),
        local_output_count: vcir.local_outputs.len(),
        child_count: vcir.child_entries.len(),
        related_artifacts,
    })
}

fn resolved_output_from_local(local: &VcirLocalOutput) -> AuditTraceResolvedOutput {
    AuditTraceResolvedOutput {
        output_id: local.output_id.clone(),
        output_type: local.output_type,
        rule_hash: local.rule_hash.clone(),
        source_object_uri: local.source_object_uri.clone(),
        source_object_type: local.source_object_type.clone(),
        source_object_hash: local.source_object_hash.clone(),
        source_ee_cert_hash: local.source_ee_cert_hash.clone(),
        item_effective_until_rfc3339_utc: local.item_effective_until.rfc3339_utc.clone(),
        payload_json: local.payload_json.clone(),
        validation_path_hint: local.validation_path_hint.clone(),
    }
}

fn resolve_raw_ref(
    store: &RocksStore,
    sha256_hex: &str,
    fallback_uri: Option<&str>,
    fallback_object_type: Option<&str>,
) -> Result<AuditTraceRawRef, AuditTraceError> {
    let raw = store.get_raw_by_hash_entry(sha256_hex)?;
    if raw.is_some() {
        return Ok(raw_ref_from_entry(sha256_hex, raw.as_ref()));
    }

    let blob = store.get_blob_bytes(sha256_hex)?;
    match blob {
        Some(bytes) => Ok(AuditTraceRawRef {
            sha256_hex: sha256_hex.to_string(),
            raw_present: true,
            origin_uris: fallback_uri
                .map(|uri| vec![uri.to_string()])
                .unwrap_or_default(),
            object_type: fallback_object_type.map(str::to_string),
            byte_len: Some(bytes.len()),
        }),
        None => Ok(raw_ref_from_entry(sha256_hex, None)),
    }
}

fn resolve_source_ee_cert_raw_ref(
    store: &RocksStore,
    local: &VcirLocalOutput,
) -> Result<AuditTraceRawRef, AuditTraceError> {
    let raw = store.get_raw_by_hash_entry(&local.source_ee_cert_hash)?;
    if raw.is_some() {
        return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, raw.as_ref()));
    }

    let source_bytes = store.get_blob_bytes(&local.source_object_hash)?;
    let Some(source_bytes) = source_bytes else {
        return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None));
    };

    let derived = match local.source_object_type.as_str() {
        "roa" => RoaObject::decode_der(&source_bytes).ok().and_then(|roa| {
            roa.signed_object
                .signed_data
                .certificates
                .first()
                .map(|cert| cert.raw_der.to_vec())
        }),
        "aspa" => AspaObject::decode_der(&source_bytes).ok().and_then(|aspa| {
            aspa.signed_object
                .signed_data
                .certificates
                .first()
                .map(|cert| cert.raw_der.to_vec())
        }),
        "mft" => ManifestObject::decode_der(&source_bytes)
            .ok()
            .and_then(|manifest| {
                manifest
                    .signed_object
                    .signed_data
                    .certificates
                    .first()
                    .map(|cert| cert.raw_der.to_vec())
            }),
        "router_key" => Some(source_bytes),
        _ => None,
    };

    let Some(ee_der) = derived else {
        return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None));
    };
    if crate::audit::sha256_hex(ee_der.as_slice()) != local.source_ee_cert_hash {
        return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None));
    }

    Ok(AuditTraceRawRef {
        sha256_hex: local.source_ee_cert_hash.clone(),
        raw_present: true,
        origin_uris: Vec::new(),
        object_type: Some("cer".to_string()),
        byte_len: Some(ee_der.len()),
    })
}

fn raw_ref_from_entry(sha256_hex: &str, entry: Option<&RawByHashEntry>) -> AuditTraceRawRef {
    match entry {
        Some(entry) => AuditTraceRawRef {
            sha256_hex: sha256_hex.to_string(),
            raw_present: true,
            origin_uris: entry.origin_uris.clone(),
            object_type: entry.object_type.clone(),
            byte_len: Some(entry.bytes.len()),
        },
        None => AuditTraceRawRef {
            sha256_hex: sha256_hex.to_string(),
            raw_present: false,
            origin_uris: Vec::new(),
            object_type: None,
            byte_len: None,
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audit::sha256_hex;
    use crate::data_model::roa::RoaObject;
    use crate::storage::{
        PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirCcrManifestProjection,
        VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
    };
    use base64::Engine as _;

    fn sample_vcir(
        manifest_rsync_uri: &str,
        parent_manifest_rsync_uri: Option<&str>,
        tal_id: &str,
        local_output: Option<VcirLocalOutput>,
        related_artifacts: Vec<VcirRelatedArtifact>,
    ) -> ValidatedCaInstanceResult {
        let now = time::OffsetDateTime::now_utc();
        let next = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
        let local_outputs: Vec<VcirLocalOutput> = local_output.into_iter().collect();
        let ccr_manifest_projection = VcirCcrManifestProjection {
            manifest_rsync_uri: manifest_rsync_uri.to_string(),
            manifest_sha256: vec![0x44; 32],
            manifest_size: 2048,
            manifest_ee_aki: vec![0x55; 20],
            manifest_number_be: vec![1],
            manifest_this_update: PackTime::from_utc_offset_datetime(now),
            manifest_sia_locations_der: vec![vec![
                0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
                b'r', b's', b'y', b'n', b'c',
            ]],
            subordinate_skis: vec![vec![0x33; 20]],
        };
        ValidatedCaInstanceResult {
            manifest_rsync_uri: manifest_rsync_uri.to_string(),
            parent_manifest_rsync_uri: parent_manifest_rsync_uri.map(str::to_string),
            tal_id: tal_id.to_string(),
            ca_subject_name: format!("CN={manifest_rsync_uri}"),
            ca_ski: "11".repeat(20),
            issuer_ski: "22".repeat(20),
            last_successful_validation_time: PackTime::from_utc_offset_datetime(now),
            current_manifest_rsync_uri: manifest_rsync_uri.to_string(),
            current_crl_rsync_uri: manifest_rsync_uri.replace(".mft", ".crl"),
            validated_manifest_meta: ValidatedManifestMeta {
                validated_manifest_number: vec![1],
                validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
                validated_manifest_next_update: next.clone(),
            },
            ccr_manifest_projection,
            instance_gate: VcirInstanceGate {
                manifest_next_update: next.clone(),
                current_crl_next_update: next.clone(),
                self_ca_not_after: PackTime::from_utc_offset_datetime(
                    now + time::Duration::hours(2),
                ),
                instance_effective_until: next,
            },
            child_entries: vec![VcirChildEntry {
                child_manifest_rsync_uri: "rsync://example.test/child/child.mft".to_string(),
                child_cert_rsync_uri: "rsync://example.test/parent/child.cer".to_string(),
                child_cert_hash: sha256_hex(b"child-cert"),
                child_ski: "33".repeat(20),
                child_rsync_base_uri: "rsync://example.test/child/".to_string(),
                child_publication_point_rsync_uri: "rsync://example.test/child/".to_string(),
                child_rrdp_notification_uri: Some(
                    "https://example.test/child/notify.xml".to_string(),
                ),
                child_effective_ip_resources: None,
                child_effective_as_resources: None,
                accepted_at_validation_time: PackTime::from_utc_offset_datetime(now),
            }],
            summary: VcirSummary {
                local_vrp_count: local_outputs
                    .iter()
                    .filter(|output| output.output_type == VcirOutputType::Vrp)
                    .count() as u32,
                local_aspa_count: local_outputs
                    .iter()
                    .filter(|output| output.output_type == VcirOutputType::Aspa)
                    .count() as u32,
                local_router_key_count: local_outputs
                    .iter()
                    .filter(|output| output.output_type == VcirOutputType::RouterKey)
                    .count() as u32,
                child_count: 1,
                accepted_object_count: related_artifacts.len() as u32,
                rejected_object_count: 0,
            },
            local_outputs,
            related_artifacts,
            audit_summary: VcirAuditSummary {
                failed_fetch_eligible: true,
                last_failed_fetch_reason: None,
                warning_count: 0,
                audit_flags: Vec::new(),
            },
        }
    }

    fn sample_local_output(manifest_rsync_uri: &str) -> VcirLocalOutput {
        let now = time::OffsetDateTime::now_utc();
        VcirLocalOutput {
            output_id: sha256_hex(b"vrp-output"),
            output_type: VcirOutputType::Vrp,
            item_effective_until: PackTime::from_utc_offset_datetime(
                now + time::Duration::minutes(30),
            ),
            source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
            source_object_type: "roa".to_string(),
            source_object_hash: sha256_hex(b"roa-raw"),
            source_ee_cert_hash: sha256_hex(b"roa-ee"),
            payload_json:
                serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
                    .to_string(),
            rule_hash: sha256_hex(b"roa-rule"),
            validation_path_hint: vec![
                manifest_rsync_uri.to_string(),
                "rsync://example.test/leaf/a.roa".to_string(),
                sha256_hex(b"roa-raw"),
            ],
        }
    }

    fn sample_artifacts(manifest_rsync_uri: &str, roa_hash: &str) -> Vec<VcirRelatedArtifact> {
        vec![
            VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::Manifest,
                artifact_kind: VcirArtifactKind::Mft,
                uri: Some(manifest_rsync_uri.to_string()),
                sha256: sha256_hex(manifest_rsync_uri.as_bytes()),
                object_type: Some("mft".to_string()),
                validation_status: VcirArtifactValidationStatus::Accepted,
            },
            VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::CurrentCrl,
                artifact_kind: VcirArtifactKind::Crl,
                uri: Some(manifest_rsync_uri.replace(".mft", ".crl")),
                sha256: sha256_hex(format!("{}-crl", manifest_rsync_uri).as_bytes()),
                object_type: Some("crl".to_string()),
                validation_status: VcirArtifactValidationStatus::Accepted,
            },
            VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::SignedObject,
                artifact_kind: VcirArtifactKind::Roa,
                uri: Some("rsync://example.test/leaf/a.roa".to_string()),
                sha256: roa_hash.to_string(),
                object_type: Some("roa".to_string()),
                validation_status: VcirArtifactValidationStatus::Accepted,
            },
        ]
    }

    fn put_raw_evidence(store: &RocksStore, bytes: &[u8], uri: &str, object_type: &str) {
        let mut entry = RawByHashEntry::from_bytes(sha256_hex(bytes), bytes.to_vec());
        entry.origin_uris.push(uri.to_string());
        entry.object_type = Some(object_type.to_string());
        entry.encoding = Some("der".to_string());
        store
            .put_raw_by_hash_entry(&entry)
            .expect("put raw evidence");
    }

    fn put_blob_only(store: &RocksStore, bytes: &[u8]) {
        store
            .put_blob_bytes_batch(&[(sha256_hex(bytes), bytes.to_vec())])
            .expect("put blob bytes");
    }

    #[test]
    fn trace_rule_to_root_returns_leaf_to_root_chain_and_evidence_refs() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");

        let root_manifest = "rsync://example.test/root/root.mft";
        let leaf_manifest = "rsync://example.test/leaf/leaf.mft";
        let local = sample_local_output(leaf_manifest);
        let leaf_vcir = sample_vcir(
            leaf_manifest,
            Some(root_manifest),
            "test-tal",
            Some(local.clone()),
            sample_artifacts(leaf_manifest, &local.source_object_hash),
        );
        let root_vcir = sample_vcir(
            root_manifest,
            None,
            "test-tal",
            None,
            sample_artifacts(root_manifest, &sha256_hex(b"root-object")),
        );
        store.put_vcir(&leaf_vcir).expect("put leaf vcir");
        store.put_vcir(&root_vcir).expect("put root vcir");

        let rule_entry = AuditRuleIndexEntry {
            kind: AuditRuleKind::Roa,
            rule_hash: local.rule_hash.clone(),
            manifest_rsync_uri: leaf_manifest.to_string(),
            source_object_uri: local.source_object_uri.clone(),
            source_object_hash: local.source_object_hash.clone(),
            output_id: local.output_id.clone(),
            item_effective_until: local.item_effective_until.clone(),
        };
        store
            .put_audit_rule_index_entry(&rule_entry)
            .expect("put rule index");

        put_raw_evidence(&store, leaf_manifest.as_bytes(), leaf_manifest, "mft");
        put_raw_evidence(
            &store,
            format!("{}-crl", leaf_manifest).as_bytes(),
            &leaf_manifest.replace(".mft", ".crl"),
            "crl",
        );
        put_raw_evidence(&store, b"roa-raw", &local.source_object_uri, "roa");
        put_raw_evidence(&store, b"roa-ee", "rsync://example.test/leaf/a.ee", "cer");
        put_raw_evidence(&store, root_manifest.as_bytes(), root_manifest, "mft");
        put_raw_evidence(
            &store,
            format!("{}-crl", root_manifest).as_bytes(),
            &root_manifest.replace(".mft", ".crl"),
            "crl",
        );

        let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash)
            .expect("trace rule")
            .expect("trace exists");

        assert_eq!(trace.rule, rule_entry);
        assert_eq!(trace.resolved_output.output_id, local.output_id);
        assert_eq!(trace.chain_leaf_to_root.len(), 2);
        assert_eq!(
            trace.chain_leaf_to_root[0].manifest_rsync_uri,
            leaf_manifest
        );
        assert_eq!(
            trace.chain_leaf_to_root[1].manifest_rsync_uri,
            root_manifest
        );
        assert_eq!(
            trace.chain_leaf_to_root[0]
                .parent_manifest_rsync_uri
                .as_deref(),
            Some(root_manifest)
        );
        assert!(trace.source_object_raw.raw_present);
        assert!(trace.source_ee_cert_raw.raw_present);
        assert!(
            trace.chain_leaf_to_root[0]
                .related_artifacts
                .iter()
                .any(|artifact| {
                    artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present
                })
        );
    }

    #[test]
    fn trace_rule_to_root_supports_router_key_rules() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let manifest = "rsync://example.test/router/leaf.mft";
        let mut local = sample_local_output(manifest);
        local.output_type = VcirOutputType::RouterKey;
        local.source_object_uri = "rsync://example.test/router/router.cer".to_string();
        local.source_object_type = "router_key".to_string();
        local.payload_json = serde_json::json!({
            "as_id": 64496,
            "ski_hex": "11".repeat(20),
            "spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
        })
        .to_string();
        let mut vcir = sample_vcir(
            manifest,
            None,
            "test-tal",
            Some(local),
            sample_artifacts(manifest, &sha256_hex(b"router-object")),
        );
        vcir.local_outputs[0].output_type = VcirOutputType::RouterKey;
        vcir.local_outputs[0].source_object_uri =
            "rsync://example.test/router/router.cer".to_string();
        vcir.local_outputs[0].source_object_type = "router_key".to_string();
        vcir.local_outputs[0].payload_json = serde_json::json!({
            "as_id": 64496,
            "ski_hex": "11".repeat(20),
            "spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
        })
        .to_string();
        vcir.summary.local_vrp_count = 0;
        vcir.summary.local_router_key_count = 1;
        store.put_vcir(&vcir).expect("put vcir");
        let rule_entry = AuditRuleIndexEntry {
            kind: AuditRuleKind::RouterKey,
            rule_hash: vcir.local_outputs[0].rule_hash.clone(),
            manifest_rsync_uri: manifest.to_string(),
            source_object_uri: vcir.local_outputs[0].source_object_uri.clone(),
            source_object_hash: vcir.local_outputs[0].source_object_hash.clone(),
            output_id: vcir.local_outputs[0].output_id.clone(),
            item_effective_until: vcir.local_outputs[0].item_effective_until.clone(),
        };
        store
            .put_audit_rule_index_entry(&rule_entry)
            .expect("put rule");
        let trace = trace_rule_to_root(&store, AuditRuleKind::RouterKey, &rule_entry.rule_hash)
            .expect("trace rule")
            .expect("trace exists");
        assert_eq!(trace.rule.kind, AuditRuleKind::RouterKey);
        assert_eq!(trace.resolved_output.output_type, VcirOutputType::RouterKey);
    }

    #[test]
    fn trace_rule_to_root_lazily_derives_source_ee_cert_when_raw_is_missing() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let manifest = "rsync://example.test/leaf/leaf.mft";
        let roa_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa");
        let roa_bytes = std::fs::read(&roa_path).expect("read ROA fixture");
        let roa = RoaObject::decode_der(&roa_bytes).expect("decode ROA fixture");
        let local = VcirLocalOutput {
            output_id: sha256_hex(b"lazy-vrp-output"),
            output_type: VcirOutputType::Vrp,
            item_effective_until: PackTime::from_utc_offset_datetime(
                time::OffsetDateTime::now_utc() + time::Duration::minutes(30),
            ),
            source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
            source_object_type: "roa".to_string(),
            source_object_hash: sha256_hex(&roa_bytes),
            source_ee_cert_hash: sha256_hex(
                roa.signed_object.signed_data.certificates[0]
                    .raw_der
                    .as_slice(),
            ),
            payload_json:
                serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
                    .to_string(),
            rule_hash: sha256_hex(b"lazy-roa-rule"),
            validation_path_hint: vec![manifest.to_string()],
        };
        let vcir = sample_vcir(
            manifest,
            None,
            "test-tal",
            Some(local.clone()),
            sample_artifacts(manifest, &local.source_object_hash),
        );
        store.put_vcir(&vcir).expect("put vcir");
        let rule_entry = AuditRuleIndexEntry {
            kind: AuditRuleKind::Roa,
            rule_hash: local.rule_hash.clone(),
            manifest_rsync_uri: manifest.to_string(),
            source_object_uri: local.source_object_uri.clone(),
            source_object_hash: local.source_object_hash.clone(),
            output_id: local.output_id.clone(),
            item_effective_until: local.item_effective_until.clone(),
        };
        store
            .put_audit_rule_index_entry(&rule_entry)
            .expect("put rule index");

        put_raw_evidence(&store, manifest.as_bytes(), manifest, "mft");
        put_raw_evidence(
            &store,
            format!("{}-crl", manifest).as_bytes(),
            &manifest.replace(".mft", ".crl"),
            "crl",
        );
        put_raw_evidence(&store, &roa_bytes, &local.source_object_uri, "roa");

        let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash)
            .expect("trace rule")
            .expect("trace exists");
        assert!(trace.source_object_raw.raw_present);
        assert!(trace.source_ee_cert_raw.raw_present);
        assert_eq!(trace.source_ee_cert_raw.object_type.as_deref(), Some("cer"));
    }

    #[test]
    fn trace_rule_to_root_uses_blob_only_fallback_for_source_object_raw() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let main_db = store_dir.path().join("main-db");
        let raw_db = store_dir.path().join("raw-store.db");
        let store =
            RocksStore::open_with_external_raw_store(&main_db, &raw_db).expect("open rocksdb");
        let manifest = "rsync://example.test/leaf/leaf.mft";
        let roa_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa");
        let roa_bytes = std::fs::read(&roa_path).expect("read ROA fixture");
        let roa = RoaObject::decode_der(&roa_bytes).expect("decode ROA fixture");
        let local = VcirLocalOutput {
            output_id: sha256_hex(b"blob-only-vrp-output"),
            output_type: VcirOutputType::Vrp,
            item_effective_until: PackTime::from_utc_offset_datetime(
                time::OffsetDateTime::now_utc() + time::Duration::minutes(30),
            ),
            source_object_uri: "rsync://example.test/leaf/blob-only.roa".to_string(),
            source_object_type: "roa".to_string(),
            source_object_hash: sha256_hex(&roa_bytes),
            source_ee_cert_hash: sha256_hex(
                roa.signed_object.signed_data.certificates[0]
                    .raw_der
                    .as_slice(),
            ),
            payload_json:
                serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
                    .to_string(),
            rule_hash: sha256_hex(b"blob-only-roa-rule"),
            validation_path_hint: vec![manifest.to_string()],
        };
        let vcir = sample_vcir(
            manifest,
            None,
            "test-tal",
            Some(local.clone()),
            sample_artifacts(manifest, &local.source_object_hash),
        );
        store.put_vcir(&vcir).expect("put vcir");
        let rule_entry = AuditRuleIndexEntry {
            kind: AuditRuleKind::Roa,
            rule_hash: local.rule_hash.clone(),
            manifest_rsync_uri: manifest.to_string(),
            source_object_uri: local.source_object_uri.clone(),
            source_object_hash: local.source_object_hash.clone(),
            output_id: local.output_id.clone(),
            item_effective_until: local.item_effective_until.clone(),
        };
        store
            .put_audit_rule_index_entry(&rule_entry)
            .expect("put rule index");

        put_raw_evidence(&store, manifest.as_bytes(), manifest, "mft");
        put_raw_evidence(
            &store,
            format!("{}-crl", manifest).as_bytes(),
            &manifest.replace(".mft", ".crl"),
            "crl",
        );
        put_blob_only(&store, &roa_bytes);

        let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash)
            .expect("trace rule")
            .expect("trace exists");
        assert!(trace.source_object_raw.raw_present);
        assert_eq!(
            trace.source_object_raw.origin_uris,
            vec![local.source_object_uri.clone()]
        );
        assert_eq!(trace.source_object_raw.object_type.as_deref(), Some("roa"));
        assert_eq!(trace.source_object_raw.byte_len, Some(roa_bytes.len()));
        assert!(trace.source_ee_cert_raw.raw_present);
    }

    #[test]
    fn trace_rule_to_root_returns_none_for_missing_rule_index() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");

        assert!(
            trace_rule_to_root(&store, AuditRuleKind::Roa, &sha256_hex(b"missing"))
                .expect("missing trace ok")
                .is_none()
        );
    }

    #[test]
    fn trace_rule_to_root_errors_when_index_points_to_missing_vcir() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let rule_hash = sha256_hex(b"missing-vcir-rule");
        store
            .put_audit_rule_index_entry(&AuditRuleIndexEntry {
                kind: AuditRuleKind::Roa,
                rule_hash: rule_hash.clone(),
                manifest_rsync_uri: "rsync://example.test/missing.mft".to_string(),
                source_object_uri: "rsync://example.test/missing.roa".to_string(),
                source_object_hash: sha256_hex(b"missing-source"),
                output_id: sha256_hex(b"missing-output"),
                item_effective_until: PackTime::from_utc_offset_datetime(
                    time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
                ),
            })
            .expect("put rule index");

        let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err();
        assert!(matches!(
            err,
            AuditTraceError::MissingVcir { manifest_rsync_uri }
            if manifest_rsync_uri == "rsync://example.test/missing.mft"
        ));
    }

    #[test]
    fn trace_rule_to_root_errors_when_vcir_local_output_is_missing() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let manifest = "rsync://example.test/leaf/leaf.mft";
        let vcir = sample_vcir(
            manifest,
            None,
            "test-tal",
            None,
            sample_artifacts(manifest, &sha256_hex(b"leaf-object")),
        );
        store.put_vcir(&vcir).expect("put vcir");
        let rule_hash = sha256_hex(b"missing-output-rule");
        store
            .put_audit_rule_index_entry(&AuditRuleIndexEntry {
                kind: AuditRuleKind::Roa,
                rule_hash: rule_hash.clone(),
                manifest_rsync_uri: manifest.to_string(),
                source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
                source_object_hash: sha256_hex(b"leaf-object"),
                output_id: sha256_hex(b"missing-output"),
                item_effective_until: PackTime::from_utc_offset_datetime(
                    time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
                ),
            })
            .expect("put rule index");

        let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err();
        assert!(matches!(err, AuditTraceError::MissingLocalOutput { .. }));
    }

    #[test]
    fn trace_vcir_chain_to_root_detects_parent_cycle() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let a_manifest = "rsync://example.test/a.mft";
        let b_manifest = "rsync://example.test/b.mft";
        let a_vcir = sample_vcir(
            a_manifest,
            Some(b_manifest),
            "test-tal",
            None,
            sample_artifacts(a_manifest, &sha256_hex(b"a-object")),
        );
        let b_vcir = sample_vcir(
            b_manifest,
            Some(a_manifest),
            "test-tal",
            None,
            sample_artifacts(b_manifest, &sha256_hex(b"b-object")),
        );
        store.put_vcir(&a_vcir).expect("put a");
        store.put_vcir(&b_vcir).expect("put b");

        let err = trace_vcir_chain_to_root(&store, a_manifest).unwrap_err();
        assert!(matches!(
            err,
            AuditTraceError::ParentCycle { manifest_rsync_uri }
            if manifest_rsync_uri == a_manifest
        ));
    }
}
