use std::path::Path;

use sha2::Digest;

use rpki::data_model::manifest::ManifestObject;
use rpki::policy::{CaFailedFetchPolicy, Policy};
use rpki::storage::{
    PackTime, RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore,
    ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole,
    VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirInstanceGate,
    VcirRelatedArtifact, VcirSummary,
};
use rpki::validation::manifest::{
    ManifestProcessError, PublicationPointSource, process_manifest_publication_point,
    process_manifest_publication_point_after_repo_sync,
};

fn issuer_ca_fixture_der() -> Vec<u8> {
    std::fs::read(
        "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
    )
    .expect("read issuer ca fixture")
}

fn issuer_ca_rsync_uri() -> &'static str {
    "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"
}

fn fixture_to_rsync_uri(path: &Path) -> String {
    let rel = path
        .strip_prefix("tests/fixtures/repository")
        .expect("path under tests/fixtures/repository");
    let mut it = rel.components();
    let host = it
        .next()
        .expect("host component")
        .as_os_str()
        .to_string_lossy();
    let rest = it.as_path().to_string_lossy();
    format!("rsync://{host}/{rest}")
}

fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
    let mut s = fixture_to_rsync_uri(dir);
    if !s.ends_with('/') {
        s.push('/');
    }
    s
}

fn store_validated_manifest_baseline(
    store: &RocksStore,
    manifest_rsync_uri: &str,
    manifest_bytes: &[u8],
    manifest_number_be: Vec<u8>,
    this_update: time::OffsetDateTime,
    next_update: time::OffsetDateTime,
) {
    let manifest_sha256 = hex::encode(sha2::Sha256::digest(manifest_bytes));
    let mut manifest_raw =
        RawByHashEntry::from_bytes(manifest_sha256.clone(), manifest_bytes.to_vec());
    manifest_raw
        .origin_uris
        .push(manifest_rsync_uri.to_string());
    manifest_raw.object_type = Some("mft".to_string());
    manifest_raw.encoding = Some("der".to_string());
    store
        .put_raw_by_hash_entry(&manifest_raw)
        .expect("store VCIR manifest raw_by_hash");
    let ccr_manifest_projection = VcirCcrManifestProjection {
        manifest_rsync_uri: manifest_rsync_uri.to_string(),
        manifest_sha256: hex::decode(&manifest_sha256).expect("decode manifest hash"),
        manifest_size: manifest_bytes.len() as u64,
        manifest_ee_aki: vec![0x11; 20],
        manifest_number_be: manifest_number_be.clone(),
        manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
        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::new(),
    };

    let vcir = ValidatedCaInstanceResult {
        manifest_rsync_uri: manifest_rsync_uri.to_string(),
        parent_manifest_rsync_uri: None,
        tal_id: "test-tal".to_string(),
        ca_subject_name: "CN=test".to_string(),
        ca_ski: "aa".to_string(),
        issuer_ski: "aa".to_string(),
        last_successful_validation_time: PackTime::from_utc_offset_datetime(this_update),
        current_manifest_rsync_uri: manifest_rsync_uri.to_string(),
        current_crl_rsync_uri: format!("{manifest_rsync_uri}.crl"),
        validated_manifest_meta: ValidatedManifestMeta {
            validated_manifest_number: manifest_number_be,
            validated_manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
            validated_manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
        },
        ccr_manifest_projection,
        instance_gate: VcirInstanceGate {
            manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
            current_crl_next_update: PackTime::from_utc_offset_datetime(next_update),
            self_ca_not_after: PackTime::from_utc_offset_datetime(next_update),
            instance_effective_until: PackTime::from_utc_offset_datetime(next_update),
        },
        child_entries: Vec::new(),
        local_outputs: Vec::new(),
        related_artifacts: vec![VcirRelatedArtifact {
            artifact_role: VcirArtifactRole::Manifest,
            artifact_kind: VcirArtifactKind::Mft,
            uri: Some(manifest_rsync_uri.to_string()),
            sha256: manifest_sha256,
            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: 0,
            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)
        .expect("store validated manifest baseline");
}

fn put_current_object(store: &RocksStore, rsync_uri: &str, bytes: Vec<u8>, object_type: &str) {
    let sha256_hex = hex::encode(sha2::Sha256::digest(&bytes));
    let mut raw_entry = RawByHashEntry::from_bytes(sha256_hex.clone(), bytes);
    raw_entry.origin_uris.push(rsync_uri.to_string());
    raw_entry.object_type = Some(object_type.to_string());
    raw_entry.encoding = Some("der".to_string());
    store
        .put_raw_by_hash_entry(&raw_entry)
        .expect("store raw_by_hash");
    store
        .put_repository_view_entry(&RepositoryViewEntry {
            rsync_uri: rsync_uri.to_string(),
            current_hash: Some(sha256_hex),
            repository_source: Some("https://example.test/notification.xml".to_string()),
            object_type: Some(object_type.to_string()),
            state: RepositoryViewState::Present,
        })
        .expect("store repository view");
}

fn store_manifest_and_locked_files(
    store: &RocksStore,
    manifest_path: &Path,
    manifest_rsync_uri: &str,
    publication_point_rsync_uri: &str,
    manifest: &ManifestObject,
    manifest_bytes: &[u8],
) {
    put_current_object(store, manifest_rsync_uri, manifest_bytes.to_vec(), "mft");
    for entry in manifest
        .manifest
        .parse_files()
        .expect("parse validated manifest fileList")
        .iter()
    {
        let file_path = manifest_path
            .parent()
            .unwrap()
            .join(entry.file_name.as_str());
        let bytes = std::fs::read(&file_path)
            .unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
        let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
        put_current_object(
            &store,
            &rsync_uri,
            bytes,
            rsync_uri.rsplit('.').next().unwrap_or("bin"),
        );
    }
}

#[test]
fn repo_sync_failed_can_reuse_current_instance_vcir_when_present() {
    let manifest_path = Path::new(
        "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
    );
    let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
    let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
    let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);

    let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
    let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    store_manifest_and_locked_files(
        &store,
        manifest_path,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &manifest,
        &manifest_bytes,
    );

    let issuer_ca_der = issuer_ca_fixture_der();

    // First run: build a fresh publication point result, then seed a current-instance VCIR baseline.
    let policy = Policy::default();
    let first = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .expect("first run ok");
    assert_eq!(first.source, PublicationPointSource::Fresh);

    store_validated_manifest_baseline(
        &store,
        &manifest_rsync_uri,
        &manifest_bytes,
        manifest.manifest.manifest_number.bytes_be.clone(),
        manifest.manifest.this_update,
        manifest.manifest.next_update,
    );

    // Seed a latest current-instance VCIR baseline, then simulate RRDP/rsync repo sync failure.
    let second = process_manifest_publication_point_after_repo_sync(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
        false,
        Some("repo sync failed in test"),
    )
    .expect("repo sync failure should reuse current-instance VCIR");
    assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance);
    assert!(
        second.warnings.iter().all(|w| {
            !w.message
                .contains("using latest validated result for current CA instance")
                && !w.message.contains("manifest failed fetch")
        }),
        "successful current-instance VCIR reuse should not emit fallback bookkeeping warnings"
    );
}

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

    let mut policy = Policy::default();
    policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;

    let issuer_ca_der = issuer_ca_fixture_der();
    let err = process_manifest_publication_point_after_repo_sync(
        &store,
        &policy,
        "rsync://example.test/pp/manifest.mft",
        "rsync://example.test/pp/",
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        time::OffsetDateTime::now_utc(),
        false,
        Some("repo sync failed in test"),
    )
    .unwrap_err();
    assert!(
        err.to_string().contains("repo sync failed"),
        "unexpected error: {err}"
    );
}

#[test]
fn manifest_missing_locked_file_is_treated_as_failed_fetch() {
    let manifest_path = Path::new(
        "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
    );
    let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
    let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
    let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);

    let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
    let pp_with_slash = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
    let publication_point_rsync_uri = pp_with_slash.trim_end_matches('/'); // exercise join/pp normalization branches

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    put_current_object(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft");

    let issuer_ca_der = issuer_ca_fixture_der();
    let policy = Policy::default();
    let err = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        publication_point_rsync_uri,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .unwrap_err();
    assert!(
        matches!(err, ManifestProcessError::NoUsableCache { .. }),
        "{err}"
    );
    assert!(
        err.to_string()
            .contains("file missing in current repository view"),
        "unexpected error: {err}"
    );
}

#[test]
fn manifest_number_increases_but_this_update_not_increasing_reuses_current_instance_vcir() {
    let manifest_path = Path::new(
        "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
    );
    let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
    let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
    let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);

    let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
    let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    store_manifest_and_locked_files(
        &store,
        manifest_path,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &manifest,
        &manifest_bytes,
    );

    let issuer_ca_der = issuer_ca_fixture_der();
    let policy = Policy::default();

    // Build and store a valid publication point snapshot first.
    let _ = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .expect("first run returns validated publication point");

    // Seed VCIR with an older manifestNumber but a newer thisUpdate to trigger
    // RFC 9286 §4.2.1 thisUpdate monotonicity failure on the fresh path.
    store_validated_manifest_baseline(
        &store,
        &manifest_rsync_uri,
        &manifest_bytes,
        vec![0],
        manifest.manifest.this_update + time::Duration::hours(24),
        manifest.manifest.next_update,
    );

    let out = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .expect("should reuse current-instance VCIR");
    assert_eq!(out.source, PublicationPointSource::VcirCurrentInstance);
    assert!(
        out.warnings
            .iter()
            .any(|w| w.message.contains("thisUpdate not more recent")),
        "expected warning mentioning thisUpdate monotonicity"
    );
}

#[test]
fn manifest_number_equal_but_bytes_differ_is_rejected_without_cache() {
    let manifest_path = Path::new(
        "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
    );
    let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
    let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
    let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);

    let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
    let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    store_manifest_and_locked_files(
        &store,
        manifest_path,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &manifest,
        &manifest_bytes,
    );

    let issuer_ca_der = issuer_ca_fixture_der();
    let mut policy = Policy::default();
    policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;

    // Store a cached snapshot that has the same manifestNumber but different manifest bytes.
    let _ = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .expect("first run returns validated publication point");

    let mut mutated_manifest_bytes = manifest_bytes.clone();
    mutated_manifest_bytes[0] ^= 0xFF;
    store_validated_manifest_baseline(
        &store,
        &manifest_rsync_uri,
        &mutated_manifest_bytes,
        manifest.manifest.manifest_number.bytes_be.clone(),
        manifest.manifest.this_update,
        manifest.manifest.next_update,
    );

    let err = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .unwrap_err();
    assert!(
        matches!(err, ManifestProcessError::StopAllOutput(_)),
        "{err}"
    );
    assert!(
        err.to_string().contains("manifestNumber not higher"),
        "unexpected error: {err}"
    );
}

#[test]
fn manifest_embedded_ee_cert_path_validation_fails_with_wrong_issuer_ca() {
    let manifest_path = Path::new(
        "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
    );
    let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
    let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
    let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);

    let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
    let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    store_manifest_and_locked_files(
        &store,
        manifest_path,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &manifest,
        &manifest_bytes,
    );

    // Deliberately use a mismatched trust anchor as the "issuer CA" for this publication point.
    let wrong_issuer_ca_der =
        std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read wrong issuer fixture");

    let mut policy = Policy::default();
    policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
    let err = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &wrong_issuer_ca_der,
        None,
        validation_time,
    )
    .unwrap_err();
    assert!(
        err.to_string().contains("path validation failed"),
        "unexpected error: {err}"
    );
}
