use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use time::format_description::well_known::Rfc3339;

use crate::parallel::types::TalInputSpec;
use crate::policy::{CaFailedFetchPolicy, Policy};
use crate::fetch::rsync_system::RsyncScopePolicy;
use crate::storage::{
    RepositoryBlobVerificationSummary, RocksStore, ValidatedCaInstanceResult,
    VcirFailedFetchReuseIdentity, VcirReuseRecordsCleared,
};

pub const VALIDATION_CONTRACT_SCHEMA_VERSION: u32 = 1;
pub const CURRENT_STATE_BINDING_SCHEMA_VERSION: u32 = 1;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationCacheContract {
    pub publication_point: bool,
    pub roa: bool,
    pub child_certificate: bool,
    pub transport_prefetch: bool,
    #[serde(default)]
    pub crypto_signature: bool,
}

/// The normal-run fallback inputs which verification-only is permitted to
/// replay.  The payload remains in the source work-db; the contract binds the
/// selected VCIR and its failed-fetch identity by canonical CBOR hashes.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FallbackReplaySelection {
    pub manifest_rsync_uri: String,
    pub vcir_sha256: String,
    pub reuse_identity_sha256: String,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FallbackReplayMaterialization {
    pub contract_selection_count: u64,
    pub source_vcir_records_cleared: u64,
    pub source_reuse_identity_records_cleared: u64,
    pub imported_selection_count: u64,
    pub scratch_vcir_count: u64,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FallbackReplaySelectionComparison {
    pub matches: bool,
    pub expected: Vec<FallbackReplaySelection>,
    pub actual: Vec<FallbackReplaySelection>,
    pub missing: Vec<FallbackReplaySelection>,
    pub unexpected: Vec<FallbackReplaySelection>,
}

impl ValidationCacheContract {
    pub fn any_validation_cache_enabled(&self) -> bool {
        self.publication_point || self.roa || self.child_certificate || self.crypto_signature
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationContract {
    pub schema_version: u32,
    pub validation_time: String,
    pub binary_sha256: String,
    pub policy: Policy,
    pub max_ca_depth: usize,
    pub max_instances: Option<usize>,
    pub cache: ValidationCacheContract,
    /// Source transport scope. With `transportPrefetch=true`, verification-only
    /// replays this scope over the frozen repository view rather than deriving
    /// a new publication-point scope.
    #[serde(default)]
    pub rsync_scope_policy: RsyncScopePolicy,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub fallback_replay_selections: Vec<FallbackReplaySelection>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CurrentStateBinding {
    pub schema_version: u32,
    pub current_run_id: String,
    pub source_cir_sha256: String,
    pub source_ccr_sha256: String,
    pub validation_contract_sha256: String,
    pub updated_at_rfc3339_utc: String,
}

impl CurrentStateBinding {
    pub fn validate(&self) -> Result<(), String> {
        if self.schema_version != CURRENT_STATE_BINDING_SCHEMA_VERSION {
            return Err(format!(
                "unsupported current state binding schemaVersion: {}",
                self.schema_version
            ));
        }
        if self.current_run_id.trim().is_empty() {
            return Err("current state binding currentRunId must not be empty".to_string());
        }
        validate_sha256_hex(
            "current state binding sourceCirSha256",
            &self.source_cir_sha256,
        )?;
        validate_sha256_hex(
            "current state binding sourceCcrSha256",
            &self.source_ccr_sha256,
        )?;
        validate_sha256_hex(
            "current state binding validationContractSha256",
            &self.validation_contract_sha256,
        )?;
        time::OffsetDateTime::parse(&self.updated_at_rfc3339_utc, &Rfc3339).map_err(|error| {
            format!("invalid current state binding updatedAtRfc3339Utc: {error}")
        })?;
        Ok(())
    }
}

impl ValidationContract {
    pub fn for_current_binary(
        validation_time: time::OffsetDateTime,
        policy: Policy,
        max_ca_depth: usize,
        max_instances: Option<usize>,
        cache: ValidationCacheContract,
        rsync_scope_policy: RsyncScopePolicy,
    ) -> Result<Self, String> {
        Ok(Self {
            schema_version: VALIDATION_CONTRACT_SCHEMA_VERSION,
            validation_time: format_validation_time(validation_time)?,
            binary_sha256: current_binary_sha256()?,
            policy,
            max_ca_depth,
            max_instances,
            cache,
            rsync_scope_policy,
            fallback_replay_selections: Vec::new(),
        })
    }

    pub fn validation_time(&self) -> Result<time::OffsetDateTime, String> {
        time::OffsetDateTime::parse(&self.validation_time, &Rfc3339)
            .map(|value| value.to_offset(time::UtcOffset::UTC))
            .map_err(|error| format!("invalid validation contract validationTime: {error}"))
    }

    pub fn validate(&self) -> Result<(), String> {
        if self.schema_version != VALIDATION_CONTRACT_SCHEMA_VERSION {
            return Err(format!(
                "unsupported validation contract schemaVersion: {}",
                self.schema_version
            ));
        }
        self.validation_time()?;
        validate_sha256_hex("validation contract binarySha256", &self.binary_sha256)?;
        if self.max_ca_depth == 0 {
            return Err("validation contract maxCaDepth must be greater than zero".to_string());
        }
        if !self.fallback_replay_selections.is_empty()
            && self.policy.ca_failed_fetch_policy != CaFailedFetchPolicy::ReuseCurrentInstanceVcir
        {
            return Err(
                "validation contract fallbackReplaySelections require caFailedFetchPolicy=reuse_current_instance_vcir"
                    .to_string(),
            );
        }
        let mut previous: Option<&str> = None;
        for selection in &self.fallback_replay_selections {
            if !selection.manifest_rsync_uri.starts_with("rsync://") {
                return Err(format!(
                    "validation contract fallbackReplaySelections manifestRsyncUri must be an rsync URI: {}",
                    selection.manifest_rsync_uri
                ));
            }
            validate_sha256_hex(
                "validation contract fallbackReplaySelections vcirSha256",
                &selection.vcir_sha256,
            )?;
            validate_sha256_hex(
                "validation contract fallbackReplaySelections reuseIdentitySha256",
                &selection.reuse_identity_sha256,
            )?;
            if let Some(previous) = previous {
                if previous >= selection.manifest_rsync_uri.as_str() {
                    return Err(
                        "validation contract fallbackReplaySelections must be sorted and unique by manifestRsyncUri"
                            .to_string(),
                    );
                }
            }
            previous = Some(&selection.manifest_rsync_uri);
        }
        Ok(())
    }

    pub fn require_current_binary(&self) -> Result<(), String> {
        let current = current_binary_sha256()?;
        if current != self.binary_sha256 {
            return Err(format!(
                "verification-only binary differs from source run: source={}, current={current}",
                self.binary_sha256
            ));
        }
        Ok(())
    }
}

#[derive(Clone, Debug)]
pub struct VerificationArtifacts {
    pub source_run_dir: PathBuf,
    pub source_state_root: PathBuf,
    pub output_dir: PathBuf,
    pub source_cir: PathBuf,
    pub source_ccr: PathBuf,
    pub source_contract: PathBuf,
    pub source_work_db: PathBuf,
    pub source_repo_bytes_db: PathBuf,
    pub scratch_work_db: PathBuf,
    pub report_json: PathBuf,
    pub result_cir: PathBuf,
    pub result_ccr: PathBuf,
    pub vrps_csv: PathBuf,
    pub vaps_csv: PathBuf,
    pub compare_dir: PathBuf,
    pub source_binding: PathBuf,
    pub source_run_summary: PathBuf,
    pub result_contract: PathBuf,
    pub verification_meta: PathBuf,
}

impl VerificationArtifacts {
    pub fn new(
        source_run_dir: impl Into<PathBuf>,
        source_state_root: impl Into<PathBuf>,
        output_dir: impl Into<PathBuf>,
    ) -> Self {
        let source_run_dir = source_run_dir.into();
        let source_state_root = source_state_root.into();
        let output_dir = output_dir.into();
        Self {
            source_cir: source_run_dir.join("input.cir"),
            source_ccr: source_run_dir.join("result.ccr"),
            source_contract: source_run_dir.join("validation-contract.json"),
            source_work_db: source_state_root.join("db/work-db"),
            source_repo_bytes_db: source_state_root.join("db/repo-bytes.db"),
            scratch_work_db: output_dir.join("scratch/work-db"),
            report_json: output_dir.join("report.json"),
            result_cir: output_dir.join("input.cir"),
            result_ccr: output_dir.join("result.ccr"),
            vrps_csv: output_dir.join("vrps.csv"),
            vaps_csv: output_dir.join("vaps.csv"),
            compare_dir: output_dir.join("compare"),
            source_binding: source_state_root.join("meta/current-state-binding.json"),
            source_run_summary: source_run_dir.join("run-summary.json"),
            result_contract: output_dir.join("validation-contract.json"),
            verification_meta: output_dir.join("verification-meta.json"),
            source_run_dir,
            source_state_root,
            output_dir,
        }
    }
}

pub struct PreparedVerification {
    pub artifacts: VerificationArtifacts,
    pub cir: crate::cir::CanonicalInputRepresentation,
    pub contract: ValidationContract,
    pub blob_summary: RepositoryBlobVerificationSummary,
    pub fallback_replay_materialization: FallbackReplayMaterialization,
    pub store: Option<Arc<RocksStore>>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationCcrStateComparison {
    pub name: String,
    pub source_present: bool,
    pub verification_present: bool,
    pub source_hash_hex: Option<String>,
    pub verification_hash_hex: Option<String>,
    pub matches: bool,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationCcrComparison {
    pub state_digest_match: bool,
    pub source_version: u32,
    pub verification_version: u32,
    pub source_hash_algorithm_oid: String,
    pub verification_hash_algorithm_oid: String,
    pub mismatched_states: Vec<String>,
    pub states: Vec<VerificationCcrStateComparison>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationRunMeta {
    pub schema_version: u32,
    pub status: String,
    pub source_run_id: String,
    pub validation_time: String,
    pub wall_ms: u64,
    pub repository_objects_checked: u64,
    pub repository_bytes_checked: u64,
    pub state_digest_match: bool,
    pub fallback_replay_selection_match: bool,
    pub fallback_replay_materialization: FallbackReplayMaterialization,
    pub scratch_retained: bool,
}

pub fn read_validation_contract(path: &Path) -> Result<ValidationContract, String> {
    let bytes = fs::read(path).map_err(|error| {
        format!(
            "read validation contract failed: {}: {error}",
            path.display()
        )
    })?;
    let contract: ValidationContract = serde_json::from_slice(&bytes).map_err(|error| {
        format!(
            "decode validation contract failed: {}: {error}",
            path.display()
        )
    })?;
    contract.validate()?;
    Ok(contract)
}

pub fn read_current_state_binding(path: &Path) -> Result<CurrentStateBinding, String> {
    let bytes = fs::read(path).map_err(|error| {
        format!(
            "read current state binding failed: {}: {error}",
            path.display()
        )
    })?;
    let binding: CurrentStateBinding = serde_json::from_slice(&bytes).map_err(|error| {
        format!(
            "decode current state binding failed: {}: {error}",
            path.display()
        )
    })?;
    binding.validate()?;
    Ok(binding)
}

pub fn write_validation_contract(path: &Path, contract: &ValidationContract) -> Result<(), String> {
    contract.validate()?;
    let bytes = serde_json::to_vec_pretty(contract)
        .map_err(|error| format!("encode validation contract failed: {error}"))?;
    atomic_write(path, &bytes)
}

pub fn read_cir(path: &Path) -> Result<crate::cir::CanonicalInputRepresentation, String> {
    let bytes = fs::read(path)
        .map_err(|error| format!("read source CIR failed: {}: {error}", path.display()))?;
    crate::cir::decode_cir(&bytes)
        .map_err(|error| format!("decode source CIR failed: {}: {error}", path.display()))
}

pub fn tal_inputs_from_cir(cir: &crate::cir::CanonicalInputRepresentation) -> Vec<TalInputSpec> {
    cir.trust_anchors
        .iter()
        .map(|trust_anchor| {
            TalInputSpec::from_ta_der(
                trust_anchor.tal_uri.clone(),
                trust_anchor.tal_bytes.clone(),
                trust_anchor.ta_certificate_der.clone(),
            )
        })
        .collect()
}

pub fn cir_tal_uris(cir: &crate::cir::CanonicalInputRepresentation) -> Vec<String> {
    cir.trust_anchors
        .iter()
        .map(|trust_anchor| trust_anchor.tal_uri.clone())
        .collect()
}

/// Derive the exact fallback set used by a completed tree traversal and bind
/// every selected payload to its canonical storage representation.
pub fn collect_fallback_replay_selections(
    store: &RocksStore,
    publication_points: &[crate::audit::PublicationPointAudit],
) -> Result<Vec<FallbackReplaySelection>, String> {
    let mut selections = Vec::new();
    for publication_point in publication_points {
        if publication_point.source != "vcir_current_instance" {
            continue;
        }
        selections.push(fallback_replay_selection_for_manifest(
            store,
            &publication_point.manifest_rsync_uri,
        )?);
    }
    selections.sort();
    for pair in selections.windows(2) {
        if pair[0].manifest_rsync_uri == pair[1].manifest_rsync_uri {
            return Err(format!(
                "multiple vcir_current_instance publication points use the same manifest URI: {}",
                pair[0].manifest_rsync_uri
            ));
        }
    }
    Ok(selections)
}

pub(crate) fn fallback_replay_selection_for_manifest(
    store: &RocksStore,
    manifest_rsync_uri: &str,
) -> Result<FallbackReplaySelection, String> {
    let vcir = store
        .get_vcir(manifest_rsync_uri)
        .map_err(|error| format!("load fallback VCIR failed for {manifest_rsync_uri}: {error}"))?
        .ok_or_else(|| format!("fallback VCIR is missing for {manifest_rsync_uri}"))?;
    let reuse_identity = store
        .get_vcir_failed_fetch_reuse_identity(manifest_rsync_uri)
        .map_err(|error| {
            format!("load fallback VCIR reuse identity failed for {manifest_rsync_uri}: {error}")
        })?
        .ok_or_else(|| format!("fallback VCIR reuse identity is missing for {manifest_rsync_uri}"))?;
    fallback_replay_selection_from_values(&vcir, &reuse_identity)
}

fn fallback_replay_selection_from_values(
    vcir: &ValidatedCaInstanceResult,
    reuse_identity: &VcirFailedFetchReuseIdentity,
) -> Result<FallbackReplaySelection, String> {
    Ok(FallbackReplaySelection {
        manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
        vcir_sha256: canonical_cbor_sha256("fallback VCIR", vcir)?,
        reuse_identity_sha256: canonical_cbor_sha256("fallback VCIR reuse identity", reuse_identity)?,
    })
}

fn canonical_cbor_sha256<T: Serialize>(label: &str, value: &T) -> Result<String, String> {
    let bytes = serde_cbor::to_vec(value)
        .map_err(|error| format!("serialize {label} for SHA-256 failed: {error}"))?;
    Ok(hex::encode(Sha256::digest(bytes)))
}

pub(crate) fn materialize_fallback_replay_selections(
    store: &RocksStore,
    selections: &[FallbackReplaySelection],
) -> Result<FallbackReplayMaterialization, String> {
    let mut extracted = Vec::with_capacity(selections.len());
    for selection in selections {
        let actual = fallback_replay_selection_for_manifest(store, &selection.manifest_rsync_uri)?;
        if actual != *selection {
            return Err(format!(
                "fallback replay selection hash mismatch for {}: contract vcir={}, work-db vcir={}, contract identity={}, work-db identity={}",
                selection.manifest_rsync_uri,
                selection.vcir_sha256,
                actual.vcir_sha256,
                selection.reuse_identity_sha256,
                actual.reuse_identity_sha256,
            ));
        }
        let vcir = store
            .get_vcir(&selection.manifest_rsync_uri)
            .map_err(|error| format!("reload selected fallback VCIR failed: {error}"))?
            .expect("fallback selection was just verified as present");
        let reuse_identity = store
            .get_vcir_failed_fetch_reuse_identity(&selection.manifest_rsync_uri)
            .map_err(|error| format!("reload selected fallback reuse identity failed: {error}"))?
            .expect("fallback selection was just verified as present");
        extracted.push((vcir, reuse_identity));
    }

    let VcirReuseRecordsCleared {
        vcir_records,
        failed_fetch_identity_records,
    } = store
        .clear_vcir_reuse_records()
        .map_err(|error| format!("clear scratch VCIR fallback records failed: {error}"))?;
    for (vcir, reuse_identity) in &extracted {
        store
            .put_vcir_with_failed_fetch_reuse_identity(vcir, reuse_identity)
            .map_err(|error| format!("import selected fallback VCIR failed: {error}"))?;
    }

    let actual = store
        .list_vcirs()
        .map_err(|error| format!("list scratch fallback VCIR records failed: {error}"))?
        .iter()
        .map(|vcir| {
            let reuse_identity = store
                .get_vcir_failed_fetch_reuse_identity(&vcir.manifest_rsync_uri)
                .map_err(|error| format!("read materialized fallback reuse identity failed: {error}"))?
                .ok_or_else(|| {
                    format!(
                        "materialized fallback reuse identity is missing for {}",
                        vcir.manifest_rsync_uri
                    )
                })?;
            fallback_replay_selection_from_values(vcir, &reuse_identity)
        })
        .collect::<Result<Vec<_>, String>>()?;
    if actual != selections {
        return Err(format!(
            "scratch fallback replay allowlist differs from validation contract: expected={}, actual={}",
            selections.len(),
            actual.len()
        ));
    }

    Ok(FallbackReplayMaterialization {
        contract_selection_count: selections.len() as u64,
        source_vcir_records_cleared: vcir_records,
        source_reuse_identity_records_cleared: failed_fetch_identity_records,
        imported_selection_count: extracted.len() as u64,
        scratch_vcir_count: actual.len() as u64,
    })
}

pub fn compare_fallback_replay_selections(
    expected: &[FallbackReplaySelection],
    actual: &[FallbackReplaySelection],
) -> FallbackReplaySelectionComparison {
    let missing: Vec<FallbackReplaySelection> = expected
        .iter()
        .filter(|selection| !actual.contains(selection))
        .cloned()
        .collect();
    let unexpected: Vec<FallbackReplaySelection> = actual
        .iter()
        .filter(|selection| !expected.contains(selection))
        .cloned()
        .collect();
    FallbackReplaySelectionComparison {
        matches: missing.is_empty() && unexpected.is_empty(),
        expected: expected.to_vec(),
        actual: actual.to_vec(),
        missing,
        unexpected,
    }
}

pub fn write_fallback_replay_selection_comparison(
    path: &Path,
    comparison: &FallbackReplaySelectionComparison,
) -> Result<(), String> {
    let bytes = serde_json::to_vec_pretty(comparison)
        .map_err(|error| format!("encode fallback replay selection comparison failed: {error}"))?;
    atomic_write(path, &bytes)
}

pub fn prepare_verification(
    source_run_dir: &Path,
    source_state_root: &Path,
    output_dir: &Path,
) -> Result<PreparedVerification, String> {
    if std::env::var("RPKI_STATE_EXECUTION_LOCK_HELD").as_deref() != Ok("1") {
        return Err(
            "verification-only requires the state execution lock; use scripts/verification/run_verification_only.sh"
                .to_string(),
        );
    }
    let artifacts = VerificationArtifacts::new(source_run_dir, source_state_root, output_dir);
    validate_output_isolated(&artifacts)?;
    validate_source_run_success(&artifacts.source_run_summary)?;

    let binding = read_current_state_binding(&artifacts.source_binding)?;
    let source_run_id = artifacts
        .source_run_dir
        .file_name()
        .and_then(|value| value.to_str())
        .ok_or_else(|| "source run directory must have a UTF-8 run id basename".to_string())?;
    if binding.current_run_id != source_run_id {
        return Err(format!(
            "source run is not bound to the current state: requested={source_run_id}, current={}",
            binding.current_run_id
        ));
    }
    verify_bound_file(
        "source CIR",
        &artifacts.source_cir,
        &binding.source_cir_sha256,
    )?;
    verify_bound_file(
        "source CCR",
        &artifacts.source_ccr,
        &binding.source_ccr_sha256,
    )?;
    verify_bound_file(
        "validation contract",
        &artifacts.source_contract,
        &binding.validation_contract_sha256,
    )?;

    let cir = read_cir(&artifacts.source_cir)?;
    let contract = read_validation_contract(&artifacts.source_contract)?;
    if cir.validation_time.to_offset(time::UtcOffset::UTC) != contract.validation_time()? {
        return Err(format!(
            "source CIR validation time differs from validation contract: cir={}, contract={}",
            format_validation_time(cir.validation_time)?,
            contract.validation_time
        ));
    }
    contract.require_current_binary()?;
    if !contract.cache.any_validation_cache_enabled() {
        return Err(
            "source run did not enable PP, ROA, child-certificate, or crypto signature validation cache".to_string(),
        );
    }
    if !artifacts.source_work_db.is_dir() {
        return Err(format!(
            "source work-db is missing: {}",
            artifacts.source_work_db.display()
        ));
    }
    if !artifacts.source_repo_bytes_db.is_dir() {
        return Err(format!(
            "source repo-bytes.db is missing: {}",
            artifacts.source_repo_bytes_db.display()
        ));
    }

    fs::create_dir_all(&artifacts.output_dir).map_err(|error| {
        format!(
            "create verification output directory failed: {}: {error}",
            artifacts.output_dir.display()
        )
    })?;
    RocksStore::create_read_only_checkpoint(&artifacts.source_work_db, &artifacts.scratch_work_db)
        .map_err(|error| format!("create frozen work-db checkpoint failed: {error}"))?;
    let store = Arc::new(
        RocksStore::open_with_external_repo_bytes_read_only(
            &artifacts.scratch_work_db,
            &artifacts.source_repo_bytes_db,
        )
        .map_err(|error| format!("open frozen verification store failed: {error}"))?,
    );
    let fallback_replay_materialization = materialize_fallback_replay_selections(
        store.as_ref(),
        &contract.fallback_replay_selections,
    )?;
    let blob_summary = store
        .verify_current_repository_blobs(1024)
        .map_err(|error| format!("verify frozen repository blobs failed: {error}"))?;
    write_validation_contract(&artifacts.result_contract, &contract)?;

    Ok(PreparedVerification {
        artifacts,
        cir,
        contract,
        blob_summary,
        fallback_replay_materialization,
        store: Some(store),
    })
}

pub fn compare_ccr_files(
    source_path: &Path,
    verification_path: &Path,
) -> Result<VerificationCcrComparison, String> {
    let source = fs::read(source_path)
        .map_err(|error| format!("read source CCR failed: {}: {error}", source_path.display()))?;
    let verification = fs::read(verification_path).map_err(|error| {
        format!(
            "read verification CCR failed: {}: {error}",
            verification_path.display()
        )
    })?;
    let comparison = crate::ccr::compare_state_digests(&source, &verification)
        .map_err(|error| format!("compare CCR state digests failed: {error}"))?;
    let states = comparison
        .states
        .iter()
        .map(|state| VerificationCcrStateComparison {
            name: state.name.to_string(),
            source_present: state.ours_present,
            verification_present: state.peer_present,
            source_hash_hex: state.ours_hash_hex.clone(),
            verification_hash_hex: state.peer_hash_hex.clone(),
            matches: state.matches,
        })
        .collect::<Vec<_>>();
    Ok(VerificationCcrComparison {
        state_digest_match: comparison.matches(),
        source_version: comparison.ours.version,
        verification_version: comparison.peer.version,
        source_hash_algorithm_oid: comparison.ours.hash_alg_oid.clone(),
        verification_hash_algorithm_oid: comparison.peer.hash_alg_oid.clone(),
        mismatched_states: comparison
            .mismatched_state_names()
            .into_iter()
            .map(str::to_string)
            .collect(),
        states,
    })
}

pub fn write_ccr_comparison(
    json_path: &Path,
    markdown_path: Option<&Path>,
    comparison: &VerificationCcrComparison,
) -> Result<(), String> {
    let json = serde_json::to_vec_pretty(comparison)
        .map_err(|error| format!("encode CCR comparison failed: {error}"))?;
    atomic_write(json_path, &json)?;
    if let Some(markdown_path) = markdown_path {
        let mut markdown = String::from(
            "# Verification-only CCR Comparison\n\n| State | Source hash | Verification hash | Match |\n|---|---|---|---|\n",
        );
        for state in &comparison.states {
            markdown.push_str(&format!(
                "| {} | {} | {} | {} |\n",
                state.name,
                state.source_hash_hex.as_deref().unwrap_or("absent"),
                state.verification_hash_hex.as_deref().unwrap_or("absent"),
                if state.matches { "yes" } else { "no" }
            ));
        }
        markdown.push_str(&format!(
            "\nOverall state digest match: **{}**\n",
            if comparison.state_digest_match {
                "yes"
            } else {
                "no"
            }
        ));
        atomic_write(markdown_path, markdown.as_bytes())?;
    }
    Ok(())
}

pub fn write_verification_meta(path: &Path, meta: &VerificationRunMeta) -> Result<(), String> {
    let bytes = serde_json::to_vec_pretty(meta)
        .map_err(|error| format!("encode verification metadata failed: {error}"))?;
    atomic_write(path, &bytes)
}

#[derive(Clone)]
pub struct NetworkDisabledHttpFetcher;

impl crate::sync::rrdp::Fetcher for NetworkDisabledHttpFetcher {
    fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
        Err(format!(
            "network access is disabled in verification-only mode: {uri}"
        ))
    }
}

pub fn sha256_file(path: &Path) -> Result<String, String> {
    let bytes = fs::read(path)
        .map_err(|error| format!("read file for SHA-256 failed: {}: {error}", path.display()))?;
    Ok(hex::encode(Sha256::digest(bytes)))
}

pub fn current_binary_sha256() -> Result<String, String> {
    let path = std::env::current_exe()
        .map_err(|error| format!("resolve current executable failed: {error}"))?;
    sha256_file(&path)
}

pub fn format_validation_time(value: time::OffsetDateTime) -> Result<String, String> {
    value
        .to_offset(time::UtcOffset::UTC)
        .format(&Rfc3339)
        .map_err(|error| format!("format validation time failed: {error}"))
}

fn validate_sha256_hex(label: &str, value: &str) -> Result<(), String> {
    if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(format!(
            "{label} must be a 64-character hexadecimal SHA-256"
        ));
    }
    Ok(())
}

fn validate_source_run_success(path: &Path) -> Result<(), String> {
    let bytes = fs::read(path).map_err(|error| {
        format!(
            "read source run summary failed: {}: {error}",
            path.display()
        )
    })?;
    let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|error| {
        format!(
            "decode source run summary failed: {}: {error}",
            path.display()
        )
    })?;
    if value.get("status").and_then(serde_json::Value::as_str) != Some("success") {
        return Err(format!("source run is not successful: {}", path.display()));
    }
    Ok(())
}

fn validate_output_isolated(artifacts: &VerificationArtifacts) -> Result<(), String> {
    if artifacts.output_dir.exists() {
        let mut entries = fs::read_dir(&artifacts.output_dir).map_err(|error| {
            format!(
                "read verification output directory failed: {}: {error}",
                artifacts.output_dir.display()
            )
        })?;
        if entries.next().is_some() {
            return Err(format!(
                "verification output directory must be absent or empty: {}",
                artifacts.output_dir.display()
            ));
        }
    }
    if artifacts.output_dir.starts_with(&artifacts.source_run_dir)
        || artifacts.source_run_dir.starts_with(&artifacts.output_dir)
    {
        return Err("verification output must be independent from the source run directory".into());
    }
    if let Some(run_root) = artifacts.source_state_root.parent() {
        if artifacts.output_dir.starts_with(run_root.join("runs")) {
            return Err("verification output must not use the normal runs directory".into());
        }
    }
    Ok(())
}

fn verify_bound_file(label: &str, path: &Path, expected_sha256: &str) -> Result<(), String> {
    let actual = sha256_file(path)?;
    if actual != expected_sha256 {
        return Err(format!(
            "{label} differs from current state binding: path={}, expected={}, actual={actual}",
            path.display(),
            expected_sha256
        ));
    }
    Ok(())
}

fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|error| format!("create directory failed: {}: {error}", parent.display()))?;
    }
    let tmp = path.with_extension(format!(
        "{}.tmp",
        path.extension()
            .and_then(|value| value.to_str())
            .unwrap_or("json")
    ));
    let mut file = fs::File::create(&tmp)
        .map_err(|error| format!("create temporary file failed: {}: {error}", tmp.display()))?;
    file.write_all(bytes)
        .map_err(|error| format!("write temporary file failed: {}: {error}", tmp.display()))?;
    file.write_all(b"\n")
        .map_err(|error| format!("write temporary file failed: {}: {error}", tmp.display()))?;
    file.sync_all()
        .map_err(|error| format!("sync temporary file failed: {}: {error}", tmp.display()))?;
    fs::rename(&tmp, path).map_err(|error| {
        format!(
            "rename temporary file failed: {} -> {}: {error}",
            tmp.display(),
            path.display()
        )
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ccr::{
        CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation,
        build_roa_payload_state, encode_content_info,
    };
    use crate::cir::{CanonicalInputRepresentation, CirTrustAnchor, encode_cir};
    use crate::data_model::roa::{IpPrefix, RoaAfi};
    use crate::sync::rrdp::Fetcher;
    use crate::validation::objects::Vrp;
    use std::sync::Mutex;

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    struct SourceFixture {
        _temp: tempfile::TempDir,
        source_run: PathBuf,
        state_root: PathBuf,
        output: PathBuf,
    }

    fn sample_contract() -> ValidationContract {
        ValidationContract {
            schema_version: VALIDATION_CONTRACT_SCHEMA_VERSION,
            validation_time: "2026-07-16T00:00:00Z".to_string(),
            binary_sha256: "ab".repeat(32),
            policy: Policy::default(),
            max_ca_depth: 32,
            max_instances: None,
            cache: ValidationCacheContract {
                publication_point: true,
                roa: true,
                child_certificate: true,
                transport_prefetch: false,
                crypto_signature: false,
            },
            rsync_scope_policy: RsyncScopePolicy::default(),
            fallback_replay_selections: Vec::new(),
        }
    }

    fn sample_cir() -> CanonicalInputRepresentation {
        let ta_der = b"verification-only-test-ta".to_vec();
        CanonicalInputRepresentation::new_v4(
            time::OffsetDateTime::parse(
                "2026-07-16T00:00:00Z",
                &time::format_description::well_known::Rfc3339,
            )
            .expect("validation time"),
            Vec::new(),
            Vec::new(),
            vec![CirTrustAnchor {
                ta_rsync_uri: "rsync://example.test/ta.cer".to_string(),
                tal_uri: "https://example.test/ta.tal".to_string(),
                tal_bytes: b"rsync://example.test/ta.cer\n\nAQID\n".to_vec(),
                ta_certificate_sha256: crate::cir::sha256(&ta_der),
                ta_certificate_der: ta_der,
            }],
            Vec::new(),
            Vec::new(),
        )
    }

    fn sample_ccr(asn: u32) -> Vec<u8> {
        let vrps = build_roa_payload_state(&[Vrp {
            asn,
            prefix: IpPrefix {
                afi: RoaAfi::Ipv4,
                prefix_len: 24,
                addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            },
            max_length: 24,
        }])
        .expect("build VRP state");
        encode_content_info(&CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
            version: 0,
            hash_alg: CcrDigestAlgorithm::Sha256,
            produced_at: time::OffsetDateTime::UNIX_EPOCH,
            mfts: None,
            vrps: Some(vrps),
            vaps: None,
            tas: None,
            rks: None,
        }))
        .expect("encode CCR")
    }

    fn refresh_binding(fixture: &SourceFixture) {
        let binding = CurrentStateBinding {
            schema_version: CURRENT_STATE_BINDING_SCHEMA_VERSION,
            current_run_id: "run_0001".to_string(),
            source_cir_sha256: sha256_file(&fixture.source_run.join("input.cir"))
                .expect("hash CIR"),
            source_ccr_sha256: sha256_file(&fixture.source_run.join("result.ccr"))
                .expect("hash CCR"),
            validation_contract_sha256: sha256_file(
                &fixture.source_run.join("validation-contract.json"),
            )
            .expect("hash contract"),
            updated_at_rfc3339_utc: "2026-07-16T00:01:00Z".to_string(),
        };
        fs::create_dir_all(fixture.state_root.join("meta")).expect("create meta");
        fs::write(
            fixture.state_root.join("meta/current-state-binding.json"),
            serde_json::to_vec_pretty(&binding).expect("encode binding"),
        )
        .expect("write binding");
    }

    fn source_fixture() -> SourceFixture {
        let temp = tempfile::tempdir().expect("tempdir");
        let source_run = temp.path().join("runs/run_0001");
        let state_root = temp.path().join("state");
        let output = temp.path().join("verification/run_0001");
        fs::create_dir_all(&source_run).expect("create source run");
        fs::create_dir_all(state_root.join("db")).expect("create state db root");
        let store = RocksStore::open_with_external_repo_bytes(
            &state_root.join("db/work-db"),
            &state_root.join("db/repo-bytes.db"),
        )
        .expect("create source databases");
        drop(store);
        fs::write(
            source_run.join("input.cir"),
            encode_cir(&sample_cir()).expect("encode CIR"),
        )
        .expect("write CIR");
        fs::write(source_run.join("result.ccr"), sample_ccr(64496)).expect("write CCR");
        fs::write(
            source_run.join("run-summary.json"),
            br#"{"status":"success"}"#,
        )
        .expect("write summary");
        let contract = ValidationContract::for_current_binary(
            sample_cir().validation_time,
            Policy::default(),
            32,
            None,
            ValidationCacheContract {
                publication_point: true,
                roa: false,
                child_certificate: false,
                transport_prefetch: true,
                crypto_signature: false,
            },
            RsyncScopePolicy::default(),
        )
        .expect("contract");
        write_validation_contract(&source_run.join("validation-contract.json"), &contract)
            .expect("write contract");
        let fixture = SourceFixture {
            _temp: temp,
            source_run,
            state_root,
            output,
        };
        refresh_binding(&fixture);
        fixture
    }

    fn prepare_error(fixture: &SourceFixture) -> String {
        match prepare_verification(&fixture.source_run, &fixture.state_root, &fixture.output) {
            Ok(_) => panic!("verification preparation unexpectedly succeeded"),
            Err(error) => error,
        }
    }

    #[test]
    fn validation_contract_roundtrips() {
        let dir = tempfile::tempdir().expect("tempdir");
        let path = dir.path().join("validation-contract.json");
        let expected = sample_contract();
        write_validation_contract(&path, &expected).expect("write contract");
        assert_eq!(
            read_validation_contract(&path).expect("read contract"),
            expected
        );
    }

    #[test]
    fn validation_contract_rejects_invalid_time_and_hash() {
        let mut contract = sample_contract();
        contract.validation_time = "not-a-time".to_string();
        assert!(contract.validate().is_err());
        contract.validation_time = "2026-07-16T00:00:00Z".to_string();
        contract.binary_sha256 = "bad".to_string();
        assert!(contract.validate().is_err());
    }

    #[test]
    fn fallback_replay_contract_requires_reuse_policy_and_sorted_unique_selections() {
        let selection = FallbackReplaySelection {
            manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
            vcir_sha256: "11".repeat(32),
            reuse_identity_sha256: "22".repeat(32),
        };
        let mut contract = sample_contract();
        contract.policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
        contract.fallback_replay_selections = vec![selection.clone()];
        assert!(contract
            .validate()
            .unwrap_err()
            .contains("caFailedFetchPolicy"));

        contract.policy.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;
        contract.fallback_replay_selections = vec![
            FallbackReplaySelection {
                manifest_rsync_uri: "rsync://example.test/repo/z.mft".to_string(),
                ..selection.clone()
            },
            FallbackReplaySelection {
                manifest_rsync_uri: "rsync://example.test/repo/a.mft".to_string(),
                ..selection.clone()
            },
        ];
        assert!(contract.validate().unwrap_err().contains("sorted and unique"));
        contract.fallback_replay_selections.sort();
        contract.validate().expect("sorted selection contract");
    }

    #[test]
    fn fallback_replay_selection_comparison_reports_exact_differences() {
        let expected = vec![FallbackReplaySelection {
            manifest_rsync_uri: "rsync://example.test/repo/a.mft".to_string(),
            vcir_sha256: "11".repeat(32),
            reuse_identity_sha256: "22".repeat(32),
        }];
        let mut actual = expected.clone();
        actual[0].vcir_sha256 = "33".repeat(32);
        let comparison = compare_fallback_replay_selections(&expected, &actual);
        assert!(!comparison.matches);
        assert_eq!(comparison.missing, expected);
        assert_eq!(comparison.unexpected, actual);

        let temp = tempfile::tempdir().expect("tempdir");
        let path = temp.path().join("fallback-replay-selection.json");
        write_fallback_replay_selection_comparison(&path, &comparison)
            .expect("write fallback selection comparison");
        assert!(fs::read_to_string(path)
            .expect("read fallback selection comparison")
            .contains("reuseIdentitySha256"));
    }

    #[test]
    fn cache_contract_distinguishes_prefetch_from_validation_caches() {
        let cache = ValidationCacheContract {
            publication_point: false,
            roa: false,
            child_certificate: false,
            transport_prefetch: true,
            crypto_signature: false,
        };
        assert!(!cache.any_validation_cache_enabled());
    }

    #[test]
    fn validation_contract_defaults_missing_rsync_scope_to_module_root() {
        let mut value = serde_json::to_value(sample_contract()).expect("serialize contract");
        value
            .as_object_mut()
            .expect("contract object")
            .remove("rsyncScopePolicy");
        let decoded: ValidationContract =
            serde_json::from_value(value).expect("decode older contract");
        assert_eq!(decoded.rsync_scope_policy, RsyncScopePolicy::ModuleRoot);
    }

    #[test]
    fn validation_contract_and_binding_validation_cover_rejection_paths() {
        let time = time::OffsetDateTime::parse(
            "2026-07-16T00:00:00Z",
            &time::format_description::well_known::Rfc3339,
        )
        .expect("time");
        let contract = ValidationContract::for_current_binary(
            time,
            Policy::default(),
            32,
            Some(10),
            sample_contract().cache,
            RsyncScopePolicy::default(),
        )
        .expect("current contract");
        assert_eq!(contract.validation_time().expect("contract time"), time);
        contract.validate().expect("valid contract");
        contract.require_current_binary().expect("same binary");

        let mut invalid = contract.clone();
        invalid.schema_version += 1;
        assert!(invalid.validate().unwrap_err().contains("schemaVersion"));
        invalid = contract.clone();
        invalid.max_ca_depth = 0;
        assert!(invalid.validate().unwrap_err().contains("maxCaDepth"));
        invalid = contract.clone();
        invalid.binary_sha256 = "00".repeat(32);
        assert!(invalid.require_current_binary().is_err());

        let valid_binding = CurrentStateBinding {
            schema_version: CURRENT_STATE_BINDING_SCHEMA_VERSION,
            current_run_id: "run_0001".to_string(),
            source_cir_sha256: "11".repeat(32),
            source_ccr_sha256: "22".repeat(32),
            validation_contract_sha256: "33".repeat(32),
            updated_at_rfc3339_utc: "2026-07-16T00:00:00Z".to_string(),
        };
        valid_binding.validate().expect("valid binding");
        for mutate in 0..6 {
            let mut binding = valid_binding.clone();
            match mutate {
                0 => binding.schema_version += 1,
                1 => binding.current_run_id.clear(),
                2 => binding.source_cir_sha256 = "bad".into(),
                3 => binding.source_ccr_sha256 = "bad".into(),
                4 => binding.validation_contract_sha256 = "bad".into(),
                _ => binding.updated_at_rfc3339_utc = "bad".into(),
            }
            assert!(binding.validate().is_err());
        }
    }

    #[test]
    fn artifact_helpers_read_write_compare_and_disable_network() {
        let temp = tempfile::tempdir().expect("tempdir");
        let source = temp.path().join("runs/run_0001");
        let state = temp.path().join("state");
        let output = temp.path().join("verification/run_0001");
        let artifacts = VerificationArtifacts::new(&source, &state, &output);
        assert_eq!(artifacts.source_cir, source.join("input.cir"));
        assert_eq!(
            artifacts.source_binding,
            state.join("meta/current-state-binding.json")
        );
        assert_eq!(artifacts.result_ccr, output.join("result.ccr"));

        fs::create_dir_all(&source).expect("create source");
        let cir = sample_cir();
        fs::write(&artifacts.source_cir, encode_cir(&cir).expect("encode CIR")).expect("write CIR");
        let decoded = read_cir(&artifacts.source_cir).expect("read CIR");
        assert_eq!(cir_tal_uris(&decoded), vec!["https://example.test/ta.tal"]);
        let tal_inputs = tal_inputs_from_cir(&decoded);
        assert_eq!(tal_inputs.len(), 1);

        let first = temp.path().join("first.ccr");
        let second = temp.path().join("second.ccr");
        fs::write(&first, sample_ccr(64496)).expect("first CCR");
        fs::write(&second, sample_ccr(64496)).expect("second CCR");
        let equal = compare_ccr_files(&first, &second).expect("equal compare");
        assert!(equal.state_digest_match);
        let json = temp.path().join("compare/result.json");
        let markdown = temp.path().join("compare/result.md");
        write_ccr_comparison(&json, Some(&markdown), &equal).expect("write compare");
        assert!(
            fs::read_to_string(json)
                .expect("json")
                .contains("stateDigestMatch")
        );
        assert!(
            fs::read_to_string(markdown)
                .expect("md")
                .contains("Overall state")
        );

        fs::write(&second, sample_ccr(64497)).expect("different CCR");
        let unequal = compare_ccr_files(&first, &second).expect("unequal compare");
        assert!(!unequal.state_digest_match);
        assert_eq!(unequal.mismatched_states, vec!["vrps"]);

        let meta = VerificationRunMeta {
            schema_version: 1,
            status: "success".to_string(),
            source_run_id: "run_0001".to_string(),
            validation_time: "2026-07-16T00:00:00Z".to_string(),
            wall_ms: 12,
            repository_objects_checked: 3,
            repository_bytes_checked: 4,
            state_digest_match: true,
            fallback_replay_selection_match: true,
            fallback_replay_materialization: FallbackReplayMaterialization::default(),
            scratch_retained: false,
        };
        let meta_path = temp.path().join("meta/result.json");
        write_verification_meta(&meta_path, &meta).expect("write meta");
        assert!(
            fs::read_to_string(meta_path)
                .expect("meta")
                .contains("run_0001")
        );
        assert!(
            NetworkDisabledHttpFetcher
                .fetch("https://example.test")
                .is_err()
        );
        assert_eq!(sha256_file(&first).expect("hash").len(), 64);
        assert!(read_cir(&first).is_err());
    }

    #[test]
    fn verification_preflight_rejects_stale_inputs_then_prepares_frozen_store() {
        let _guard = ENV_LOCK.lock().expect("env lock");
        let fixture = source_fixture();
        unsafe { std::env::remove_var("RPKI_STATE_EXECUTION_LOCK_HELD") };
        assert!(prepare_error(&fixture).contains("state execution lock"));
        unsafe { std::env::set_var("RPKI_STATE_EXECUTION_LOCK_HELD", "1") };

        fs::create_dir_all(&fixture.output).expect("output");
        fs::write(fixture.output.join("occupied"), b"x").expect("occupied");
        assert!(prepare_error(&fixture).contains("absent or empty"));
        fs::remove_dir_all(&fixture.output).expect("remove output");

        fs::write(
            fixture.source_run.join("run-summary.json"),
            br#"{"status":"failed"}"#,
        )
        .expect("failed summary");
        assert!(prepare_error(&fixture).contains("not successful"));
        fs::write(
            fixture.source_run.join("run-summary.json"),
            br#"{"status":"success"}"#,
        )
        .expect("success summary");

        let binding_path = fixture.state_root.join("meta/current-state-binding.json");
        let mut binding = read_current_state_binding(&binding_path).expect("binding");
        binding.current_run_id = "run_0002".to_string();
        fs::write(
            &binding_path,
            serde_json::to_vec_pretty(&binding).expect("binding JSON"),
        )
        .expect("write binding");
        assert!(prepare_error(&fixture).contains("not bound"));
        refresh_binding(&fixture);

        fs::write(fixture.source_run.join("input.cir"), b"tampered").expect("tamper CIR");
        assert!(prepare_error(&fixture).contains("differs from current state binding"));
        fs::write(
            fixture.source_run.join("input.cir"),
            encode_cir(&sample_cir()).expect("encode CIR"),
        )
        .expect("restore CIR");
        refresh_binding(&fixture);

        let mut contract =
            read_validation_contract(&fixture.source_run.join("validation-contract.json"))
                .expect("contract");
        contract.cache.publication_point = false;
        contract.cache.transport_prefetch = true;
        write_validation_contract(
            &fixture.source_run.join("validation-contract.json"),
            &contract,
        )
        .expect("write no-cache contract");
        refresh_binding(&fixture);
        assert!(prepare_error(&fixture).contains("did not enable"));

        contract.cache.roa = true;
        write_validation_contract(
            &fixture.source_run.join("validation-contract.json"),
            &contract,
        )
        .expect("restore cache contract");
        refresh_binding(&fixture);
        let mut prepared =
            prepare_verification(&fixture.source_run, &fixture.state_root, &fixture.output)
                .expect("prepare verification");
        assert_eq!(prepared.blob_summary.current_objects, 0);
        assert_eq!(prepared.blob_summary.bytes_verified, 0);
        assert_eq!(prepared.contract.validation_time, "2026-07-16T00:00:00Z");
        assert_eq!(
            prepared
                .fallback_replay_materialization
                .contract_selection_count,
            0
        );
        assert_eq!(
            prepared.fallback_replay_materialization.scratch_vcir_count,
            0
        );
        assert_eq!(cir_tal_uris(&prepared.cir).len(), 1);
        assert!(prepared.artifacts.scratch_work_db.is_dir());
        assert!(prepared.artifacts.result_contract.is_file());
        drop(prepared.store.take());
        unsafe { std::env::remove_var("RPKI_STATE_EXECUTION_LOCK_HELD") };
    }

    #[test]
    fn private_validation_helpers_report_bad_files_and_paths() {
        let temp = tempfile::tempdir().expect("tempdir");
        let summary = temp.path().join("summary.json");
        fs::write(&summary, b"not-json").expect("summary");
        assert!(validate_source_run_success(&summary).is_err());
        assert!(validate_source_run_success(&temp.path().join("missing")).is_err());

        let file = temp.path().join("value");
        fs::write(&file, b"value").expect("file");
        assert!(verify_bound_file("value", &file, &"00".repeat(32)).is_err());
        assert!(verify_bound_file("missing", &temp.path().join("missing"), "").is_err());

        let binding = temp.path().join("binding.json");
        fs::write(&binding, b"{}").expect("binding");
        assert!(read_current_state_binding(&binding).is_err());
        assert!(read_validation_contract(&binding).is_err());

        let source = temp.path().join("normal/runs/run_0001");
        let state = temp.path().join("normal/state");
        let nested_output = source.join("verification");
        let nested = VerificationArtifacts::new(&source, &state, &nested_output);
        assert!(validate_output_isolated(&nested).is_err());
        let normal_output = temp.path().join("normal/runs/run_verify");
        let normal = VerificationArtifacts::new(&source, &state, &normal_output);
        assert!(validate_output_isolated(&normal).is_err());
    }
}
