use crate::ccr::decode::{CcrDecodeError, decode_content_info};
use crate::ccr::encode::{
    encode_aspa_payload_state_payload_der, encode_manifest_state_payload_der,
    encode_roa_payload_state_payload_der, encode_router_key_state_payload_der,
    encode_trust_anchor_state_payload_der,
};
use crate::ccr::hash::verify_state_hash;
use crate::ccr::model::{CcrContentInfo, RouterKeyState, TrustAnchorState};
use crate::storage::{RocksStore, VcirArtifactRole};
use serde::Serialize;
use std::collections::BTreeSet;
use std::path::Path;

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct CcrVerifySummary {
    pub content_type_oid: String,
    pub version: u32,
    pub produced_at_rfc3339_utc: String,
    pub state_hashes_ok: bool,
    pub manifest_instances: usize,
    pub roa_payload_sets: usize,
    pub roa_vrp_count: usize,
    pub aspa_payload_sets: usize,
    pub trust_anchor_ski_count: usize,
    pub router_key_sets: usize,
    pub router_key_count: usize,
}

#[derive(Debug, thiserror::Error)]
pub enum CcrVerifyError {
    #[error("CCR decode failed: {0}")]
    Decode(#[from] CcrDecodeError),

    #[error("ManifestState hash mismatch")]
    ManifestHashMismatch,

    #[error("ROAPayloadState hash mismatch")]
    RoaHashMismatch,

    #[error("ASPAPayloadState hash mismatch")]
    AspaHashMismatch,

    #[error("TrustAnchorState hash mismatch")]
    TrustAnchorHashMismatch,

    #[error("RouterKeyState hash mismatch")]
    RouterKeyHashMismatch,

    #[error("read report json failed: {0}: {1}")]
    ReportRead(String, String),

    #[error("parse report json failed: {0}")]
    ReportParse(String),

    #[error("VRP set mismatch: only_in_ccr={only_in_ccr} only_in_report={only_in_report}")]
    ReportVrpMismatch {
        only_in_ccr: usize,
        only_in_report: usize,
    },

    #[error("ASPA set mismatch: only_in_ccr={only_in_ccr} only_in_report={only_in_report}")]
    ReportAspaMismatch {
        only_in_ccr: usize,
        only_in_report: usize,
    },

    #[error("open RocksDB failed: {0}")]
    OpenStore(String),

    #[error("list VCIRs failed: {0}")]
    ListVcirs(String),

    #[error("VCIR manifest set mismatch: only_in_ccr={only_in_ccr} only_in_vcir={only_in_vcir}")]
    VcirManifestMismatch {
        only_in_ccr: usize,
        only_in_vcir: usize,
    },
}

pub fn verify_content_info_bytes(der: &[u8]) -> Result<CcrVerifySummary, CcrVerifyError> {
    let content_info = decode_content_info(der)?;
    verify_content_info(&content_info)
}

pub fn verify_content_info(
    content_info: &CcrContentInfo,
) -> Result<CcrVerifySummary, CcrVerifyError> {
    content_info.validate().map_err(CcrDecodeError::Validate)?;
    let state_hashes_ok = true;
    let mut manifest_instances = 0usize;
    let mut roa_payload_sets = 0usize;
    let mut roa_vrp_count = 0usize;
    let mut aspa_payload_sets = 0usize;
    let mut trust_anchor_ski_count = 0usize;
    let mut router_key_sets = 0usize;
    let mut router_key_count = 0usize;

    if let Some(mfts) = &content_info.content.mfts {
        let payload_der = encode_manifest_state_payload_der(&mfts.mis)
            .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
        if !verify_state_hash(&mfts.hash, &payload_der) {
            return Err(CcrVerifyError::ManifestHashMismatch);
        }
        manifest_instances = mfts.mis.len();
    }
    if let Some(vrps) = &content_info.content.vrps {
        let payload_der = encode_roa_payload_state_payload_der(&vrps.rps)
            .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
        if !verify_state_hash(&vrps.hash, &payload_der) {
            return Err(CcrVerifyError::RoaHashMismatch);
        }
        roa_payload_sets = vrps.rps.len();
        roa_vrp_count = vrps
            .rps
            .iter()
            .map(|set| count_roa_block_entries(&set.ip_addr_blocks))
            .sum();
    }
    if let Some(vaps) = &content_info.content.vaps {
        let payload_der = encode_aspa_payload_state_payload_der(&vaps.aps)
            .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
        if !verify_state_hash(&vaps.hash, &payload_der) {
            return Err(CcrVerifyError::AspaHashMismatch);
        }
        aspa_payload_sets = vaps.aps.len();
    }
    if let Some(tas) = &content_info.content.tas {
        verify_trust_anchor_state_hash(tas)?;
        trust_anchor_ski_count = tas.skis.len();
    }
    if let Some(rks) = &content_info.content.rks {
        verify_router_key_state_hash(rks)?;
        router_key_sets = rks.rksets.len();
        router_key_count = rks.rksets.iter().map(|set| set.router_keys.len()).sum();
    }

    let produced_at_rfc3339_utc = content_info
        .content
        .produced_at
        .to_offset(time::UtcOffset::UTC)
        .format(&time::format_description::well_known::Rfc3339)
        .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;

    Ok(CcrVerifySummary {
        content_type_oid: content_info.content_type_oid.clone(),
        version: content_info.content.version,
        produced_at_rfc3339_utc,
        state_hashes_ok,
        manifest_instances,
        roa_payload_sets,
        roa_vrp_count,
        aspa_payload_sets,
        trust_anchor_ski_count,
        router_key_sets,
        router_key_count,
    })
}

pub fn verify_against_report_json_path(
    content_info: &CcrContentInfo,
    report_json_path: &Path,
) -> Result<(), CcrVerifyError> {
    let bytes = std::fs::read(report_json_path).map_err(|e| {
        CcrVerifyError::ReportRead(report_json_path.display().to_string(), e.to_string())
    })?;
    let json: serde_json::Value =
        serde_json::from_slice(&bytes).map_err(|e| CcrVerifyError::ReportParse(e.to_string()))?;

    let report_vrps = report_vrp_keys(&json)?;
    let ccr_vrps = extract_vrp_rows(content_info)?;
    let only_in_ccr = ccr_vrps.difference(&report_vrps).count();
    let only_in_report = report_vrps.difference(&ccr_vrps).count();
    if only_in_ccr != 0 || only_in_report != 0 {
        return Err(CcrVerifyError::ReportVrpMismatch {
            only_in_ccr,
            only_in_report,
        });
    }

    let report_aspas = report_aspa_keys(&json)?;
    let ccr_aspas = ccr_aspa_keys(content_info)?;
    let only_in_ccr = ccr_aspas.difference(&report_aspas).count();
    let only_in_report = report_aspas.difference(&ccr_aspas).count();
    if only_in_ccr != 0 || only_in_report != 0 {
        return Err(CcrVerifyError::ReportAspaMismatch {
            only_in_ccr,
            only_in_report,
        });
    }
    Ok(())
}

pub fn verify_against_vcir_store_path(
    content_info: &CcrContentInfo,
    db_path: &Path,
) -> Result<(), CcrVerifyError> {
    let store = RocksStore::open(db_path).map_err(|e| CcrVerifyError::OpenStore(e.to_string()))?;
    verify_against_vcir_store(content_info, &store)
}

pub fn verify_against_vcir_store(
    content_info: &CcrContentInfo,
    store: &RocksStore,
) -> Result<(), CcrVerifyError> {
    let Some(mfts) = &content_info.content.mfts else {
        return Ok(());
    };
    let vcirs = store
        .list_vcirs()
        .map_err(|e| CcrVerifyError::ListVcirs(e.to_string()))?;
    let mut vcir_hashes = BTreeSet::new();
    for vcir in vcirs {
        if let Some(artifact) = vcir.related_artifacts.iter().find(|artifact| {
            artifact.artifact_role == VcirArtifactRole::Manifest
                && artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str())
        }) {
            if let Ok(bytes) = hex::decode(&artifact.sha256) {
                vcir_hashes.insert(bytes);
            }
        }
    }
    let ccr_hashes = mfts
        .mis
        .iter()
        .map(|mi| mi.hash.clone())
        .collect::<BTreeSet<_>>();
    let only_in_ccr = ccr_hashes.difference(&vcir_hashes).count();
    let only_in_vcir = vcir_hashes.difference(&ccr_hashes).count();
    if only_in_ccr != 0 || only_in_vcir != 0 {
        return Err(CcrVerifyError::VcirManifestMismatch {
            only_in_ccr,
            only_in_vcir,
        });
    }
    Ok(())
}

fn verify_trust_anchor_state_hash(state: &TrustAnchorState) -> Result<(), CcrVerifyError> {
    let payload_der = encode_trust_anchor_state_payload_der(&state.skis)
        .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
    if !verify_state_hash(&state.hash, &payload_der) {
        return Err(CcrVerifyError::TrustAnchorHashMismatch);
    }
    Ok(())
}

fn verify_router_key_state_hash(state: &RouterKeyState) -> Result<(), CcrVerifyError> {
    let payload_der = encode_router_key_state_payload_der(&state.rksets)
        .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
    if !verify_state_hash(&state.hash, &payload_der) {
        return Err(CcrVerifyError::RouterKeyHashMismatch);
    }
    Ok(())
}

fn report_vrp_keys(
    json: &serde_json::Value,
) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
    let mut out = BTreeSet::new();
    let Some(items) = json.get("vrps").and_then(|v| v.as_array()) else {
        return Ok(out);
    };
    for item in items {
        let asn = item
            .get("asn")
            .and_then(|v| v.as_u64())
            .ok_or_else(|| CcrVerifyError::ReportParse("vrps[].asn missing".into()))?
            as u32;
        let prefix = item
            .get("prefix")
            .and_then(|v| v.as_str())
            .ok_or_else(|| CcrVerifyError::ReportParse("vrps[].prefix missing".into()))?
            .to_string();
        let max_length = item
            .get("max_length")
            .and_then(|v| v.as_u64())
            .ok_or_else(|| CcrVerifyError::ReportParse("vrps[].max_length missing".into()))?
            as u16;
        out.insert((asn, prefix, max_length));
    }
    Ok(out)
}

fn report_aspa_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, Vec<u32>)>, CcrVerifyError> {
    let mut out = BTreeSet::new();
    let Some(items) = json.get("aspas").and_then(|v| v.as_array()) else {
        return Ok(out);
    };
    for item in items {
        let customer = item
            .get("customer_as_id")
            .and_then(|v| v.as_u64())
            .ok_or_else(|| CcrVerifyError::ReportParse("aspas[].customer_as_id missing".into()))?
            as u32;
        let mut providers = item
            .get("provider_as_ids")
            .and_then(|v| v.as_array())
            .ok_or_else(|| CcrVerifyError::ReportParse("aspas[].provider_as_ids missing".into()))?
            .iter()
            .map(|v| {
                v.as_u64()
                    .ok_or_else(|| CcrVerifyError::ReportParse("provider_as_ids[] invalid".into()))
                    .map(|v| v as u32)
            })
            .collect::<Result<Vec<_>, _>>()?;
        providers.sort_unstable();
        providers.dedup();
        out.insert((customer, providers));
    }
    Ok(out)
}

pub fn extract_vrp_rows(
    content_info: &CcrContentInfo,
) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
    let mut out = BTreeSet::new();
    let Some(vrps) = &content_info.content.vrps else {
        return Ok(out);
    };
    for set in &vrps.rps {
        for block in &set.ip_addr_blocks {
            let (afi, entries) = decode_roa_family_block(block)?;
            for (prefix_len, addr_bytes, max_len) in entries {
                let prefix = format_prefix(afi, &addr_bytes, prefix_len)?;
                out.insert((set.as_id, prefix, max_len.unwrap_or(prefix_len as u16)));
            }
        }
    }
    Ok(out)
}

fn ccr_aspa_keys(
    content_info: &CcrContentInfo,
) -> Result<BTreeSet<(u32, Vec<u32>)>, CcrVerifyError> {
    let mut out = BTreeSet::new();
    let Some(vaps) = &content_info.content.vaps else {
        return Ok(out);
    };
    for set in &vaps.aps {
        out.insert((set.customer_as_id, set.providers.clone()));
    }
    Ok(out)
}

fn decode_roa_family_block(
    block: &[u8],
) -> Result<(u16, Vec<(u8, Vec<u8>, Option<u16>)>), CcrVerifyError> {
    let mut top = crate::data_model::common::DerReader::new(block);
    let mut seq = top
        .take_sequence()
        .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
    if !top.is_empty() {
        return Err(CcrVerifyError::Decode(CcrDecodeError::Parse(
            "trailing bytes after ROAIPAddressFamily".into(),
        )));
    }
    let afi_bytes = seq
        .take_octet_string()
        .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
    let afi = u16::from_be_bytes([afi_bytes[0], afi_bytes[1]]);
    let mut addrs = seq
        .take_sequence()
        .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
    let mut entries = Vec::new();
    while !addrs.is_empty() {
        let mut addr_seq = addrs
            .take_sequence()
            .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
        let (unused_bits, content) = addr_seq
            .take_bit_string()
            .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
        let prefix_len = (content.len() * 8) as u8 - unused_bits;
        let max_len = if addr_seq.is_empty() {
            None
        } else {
            Some(
                addr_seq
                    .take_uint_u64()
                    .map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?
                    as u16,
            )
        };
        entries.push((prefix_len, content.to_vec(), max_len));
    }
    Ok((afi, entries))
}

fn format_prefix(afi: u16, addr_bytes: &[u8], prefix_len: u8) -> Result<String, CcrVerifyError> {
    match afi {
        1 => {
            let mut full = [0u8; 4];
            full[..addr_bytes.len()].copy_from_slice(addr_bytes);
            Ok(format!("{}/{prefix_len}", std::net::Ipv4Addr::from(full)))
        }
        2 => {
            let mut full = [0u8; 16];
            full[..addr_bytes.len()].copy_from_slice(addr_bytes);
            Ok(format!("{}/{prefix_len}", std::net::Ipv6Addr::from(full)))
        }
        other => Err(CcrVerifyError::Decode(CcrDecodeError::Parse(format!(
            "unsupported AFI {other}"
        )))),
    }
}

fn count_roa_block_entries(blocks: &[Vec<u8>]) -> usize {
    blocks
        .iter()
        .map(|block| {
            decode_roa_family_block(block)
                .map(|(_, entries)| entries.len())
                .unwrap_or(0)
        })
        .sum()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ccr::build::{build_aspa_payload_state, build_roa_payload_state};
    use crate::ccr::encode::{
        encode_manifest_state_payload_der, encode_roa_payload_state_payload_der,
        encode_router_key_state_payload_der, encode_trust_anchor_state_payload_der,
    };
    use crate::ccr::model::{
        CcrDigestAlgorithm, ManifestInstance, ManifestState, RouterKey, RouterKeySet,
        RpkiCanonicalCacheRepresentation,
    };
    use crate::data_model::common::BigUnsigned;
    use crate::data_model::roa::{IpPrefix, RoaAfi};
    use crate::storage::{
        PackTime, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind,
        VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
        VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact,
        VcirSummary,
    };
    use crate::validation::objects::{AspaAttestation, Vrp};

    fn sample_time() -> time::OffsetDateTime {
        time::OffsetDateTime::parse(
            "2026-03-24T00:00:00Z",
            &time::format_description::well_known::Rfc3339,
        )
        .expect("time")
    }

    fn sample_content_info() -> CcrContentInfo {
        let mis = vec![ManifestInstance {
            hash: vec![0x10; 32],
            size: 2048,
            aki: vec![0x20; 20],
            manifest_number: BigUnsigned { bytes_be: vec![1] },
            this_update: sample_time(),
            locations: vec![vec![0x30, 0x00]],
            subordinates: vec![vec![0x30; 20]],
        }];
        let mfts = ManifestState {
            most_recent_update: sample_time(),
            hash: crate::ccr::compute_state_hash(&encode_manifest_state_payload_der(&mis).unwrap()),
            mis,
        };
        let vrps = build_roa_payload_state(&[Vrp {
            asn: 64496,
            prefix: IpPrefix {
                afi: RoaAfi::Ipv4,
                prefix_len: 0,
                addr: [0; 16],
            },
            max_length: 0,
        }])
        .expect("build roa state");
        let vaps = build_aspa_payload_state(&[AspaAttestation {
            customer_as_id: 64496,
            provider_as_ids: vec![64497],
        }])
        .expect("build aspa state");
        let skis = vec![vec![0x11; 20]];
        let tas = TrustAnchorState {
            hash: crate::ccr::compute_state_hash(
                &encode_trust_anchor_state_payload_der(&skis).unwrap(),
            ),
            skis,
        };
        let rksets = vec![RouterKeySet {
            as_id: 64496,
            router_keys: vec![RouterKey {
                ski: vec![0x22; 20],
                spki_der: vec![0x30, 0x00],
            }],
        }];
        let rks = RouterKeyState {
            hash: crate::ccr::compute_state_hash(
                &encode_router_key_state_payload_der(&rksets).unwrap(),
            ),
            rksets,
        };
        CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
            version: 0,
            hash_alg: CcrDigestAlgorithm::Sha256,
            produced_at: sample_time(),
            mfts: Some(mfts),
            vrps: Some(vrps),
            vaps: Some(vaps),
            tas: Some(tas),
            rks: Some(rks),
        })
    }

    fn sample_ccr_manifest_projection(manifest_rsync_uri: &str) -> VcirCcrManifestProjection {
        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(sample_time()),
            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]],
        }
    }

    #[test]
    fn verify_detects_each_state_hash_mismatch() {
        let mut ci = sample_content_info();
        ci.content.vrps.as_mut().unwrap().hash[0] ^= 0x01;
        assert!(matches!(
            verify_content_info(&ci),
            Err(CcrVerifyError::RoaHashMismatch)
        ));

        let mut ci = sample_content_info();
        ci.content.vaps.as_mut().unwrap().hash[0] ^= 0x01;
        assert!(matches!(
            verify_content_info(&ci),
            Err(CcrVerifyError::AspaHashMismatch)
        ));

        let mut ci = sample_content_info();
        ci.content.tas.as_mut().unwrap().hash[0] ^= 0x01;
        assert!(matches!(
            verify_content_info(&ci),
            Err(CcrVerifyError::TrustAnchorHashMismatch)
        ));

        let mut ci = sample_content_info();
        ci.content.rks.as_mut().unwrap().hash[0] ^= 0x01;
        assert!(matches!(
            verify_content_info(&ci),
            Err(CcrVerifyError::RouterKeyHashMismatch)
        ));
    }

    #[test]
    fn verify_against_report_json_accepts_matching_report_and_rejects_parse_errors() {
        let td = tempfile::tempdir().expect("tempdir");
        let report = serde_json::json!({
            "vrps": [{"asn": 64496, "prefix": "0.0.0.0/0", "max_length": 0}],
            "aspas": [{"customer_as_id": 64496, "provider_as_ids": [64497]}]
        });
        let report_path = td.path().join("report.json");
        std::fs::write(&report_path, serde_json::to_vec(&report).unwrap()).unwrap();
        verify_against_report_json_path(&sample_content_info(), &report_path)
            .expect("matching report");

        let bad_path = td.path().join("bad.json");
        std::fs::write(&bad_path, b"not-json").unwrap();
        assert!(matches!(
            verify_against_report_json_path(&sample_content_info(), &bad_path),
            Err(CcrVerifyError::ReportParse(_))
        ));
    }

    #[test]
    fn verify_against_report_json_rejects_missing_fields_and_aspa_mismatch() {
        let td = tempfile::tempdir().expect("tempdir");
        let missing =
            serde_json::json!({"vrps":[{"prefix":"0.0.0.0/0","max_length":0}],"aspas":[]});
        let missing_path = td.path().join("missing.json");
        std::fs::write(&missing_path, serde_json::to_vec(&missing).unwrap()).unwrap();
        assert!(matches!(
            verify_against_report_json_path(&sample_content_info(), &missing_path),
            Err(CcrVerifyError::ReportParse(_))
        ));

        let mismatch = serde_json::json!({
            "vrps": [{"asn": 64496, "prefix": "0.0.0.0/0", "max_length": 0}],
            "aspas": [{"customer_as_id": 64496, "provider_as_ids": [65000]}]
        });
        let mismatch_path = td.path().join("mismatch.json");
        std::fs::write(&mismatch_path, serde_json::to_vec(&mismatch).unwrap()).unwrap();
        assert!(matches!(
            verify_against_report_json_path(&sample_content_info(), &mismatch_path),
            Err(CcrVerifyError::ReportAspaMismatch { .. })
        ));
    }

    #[test]
    fn verify_against_vcir_store_rejects_mismatched_manifest_hashes() {
        let td = tempfile::tempdir().expect("tempdir");
        let store = RocksStore::open(td.path()).expect("open db");
        let vcir = ValidatedCaInstanceResult {
            manifest_rsync_uri: "rsync://example.test/current.mft".to_string(),
            parent_manifest_rsync_uri: None,
            tal_id: "apnic".to_string(),
            ca_subject_name: "CN=test".to_string(),
            ca_ski: "11".repeat(20),
            issuer_ski: "22".repeat(20),
            last_successful_validation_time: PackTime::from_utc_offset_datetime(sample_time()),
            current_manifest_rsync_uri: "rsync://example.test/current.mft".to_string(),
            current_crl_rsync_uri: "rsync://example.test/current.crl".to_string(),
            validated_manifest_meta: ValidatedManifestMeta {
                validated_manifest_number: vec![1],
                validated_manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
                validated_manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
            },
            ccr_manifest_projection: sample_ccr_manifest_projection(
                "rsync://example.test/current.mft",
            ),
            instance_gate: VcirInstanceGate {
                manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
                current_crl_next_update: PackTime::from_utc_offset_datetime(sample_time()),
                self_ca_not_after: PackTime::from_utc_offset_datetime(sample_time()),
                instance_effective_until: PackTime::from_utc_offset_datetime(sample_time()),
            },
            child_entries: vec![VcirChildEntry {
                child_manifest_rsync_uri: "rsync://example.test/child.mft".to_string(),
                child_cert_rsync_uri: "rsync://example.test/child.cer".to_string(),
                child_cert_hash: "aa".repeat(32),
                child_ski: "33".repeat(20),
                child_rsync_base_uri: "rsync://example.test/".to_string(),
                child_publication_point_rsync_uri: "rsync://example.test/".to_string(),
                child_rrdp_notification_uri: None,
                child_effective_ip_resources: None,
                child_effective_as_resources: None,
                accepted_at_validation_time: PackTime::from_utc_offset_datetime(sample_time()),
            }],
            local_outputs: Vec::new(),
            related_artifacts: vec![VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::Manifest,
                artifact_kind: VcirArtifactKind::Mft,
                uri: Some("rsync://example.test/current.mft".to_string()),
                sha256: "ff".repeat(32),
                object_type: Some("mft".to_string()),
                validation_status: VcirArtifactValidationStatus::Accepted,
                reject_reason: None,
            }],
            summary: VcirSummary {
                local_vrp_count: 0,
                local_aspa_count: 0,
                local_router_key_count: 0,
                child_count: 1,
                accepted_object_count: 1,
                rejected_object_count: 0,
            },
            audit_summary: VcirAuditSummary {
                failed_fetch_eligible: true,
                last_failed_fetch_reason: None,
                warning_count: 0,
                audit_flags: Vec::new(),
            },
        };
        store.put_vcir(&vcir).unwrap();
        assert!(matches!(
            verify_against_vcir_store(&sample_content_info(), &store),
            Err(CcrVerifyError::VcirManifestMismatch { .. })
        ));
    }

    #[test]
    fn verify_vrp_helpers_reject_bad_afi_and_count_invalid_block_as_zero() {
        let block = vec![0x30, 0x08, 0x04, 0x02, 0x00, 0x63, 0x30, 0x02, 0x03, 0x00];
        let ci = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
            version: 0,
            hash_alg: CcrDigestAlgorithm::Sha256,
            produced_at: sample_time(),
            mfts: None,
            vrps: Some(crate::ccr::model::RoaPayloadState {
                rps: vec![crate::ccr::model::RoaPayloadSet {
                    as_id: 64496,
                    ip_addr_blocks: vec![block.clone()],
                }],
                hash: crate::ccr::compute_state_hash(
                    &encode_roa_payload_state_payload_der(&[crate::ccr::model::RoaPayloadSet {
                        as_id: 64496,
                        ip_addr_blocks: vec![block],
                    }])
                    .unwrap(),
                ),
            }),
            vaps: None,
            tas: None,
            rks: None,
        });
        assert!(matches!(
            extract_vrp_rows(&ci),
            Err(CcrVerifyError::Decode(_))
        ));
        let bad_count = count_roa_block_entries(&[vec![0x04, 0x00]]);
        assert_eq!(bad_count, 0);
    }
}
