use rpki::fetch::rsync::LocalDirRsyncFetcher;
use rpki::parallel::config::ParallelPhase2Config;
use rpki::policy::{Policy, SignedObjectFailurePolicy, SyncPreference};
use rpki::storage::{PackFile, PackTime, RocksStore, VcirOutputType};
use rpki::sync::repo::sync_publication_point;
use rpki::sync::rrdp::Fetcher;
use rpki::validation::manifest::process_manifest_publication_point;
use rpki::validation::objects::{
    process_publication_point_snapshot_for_issuer,
    process_publication_point_snapshot_for_issuer_parallel_roa,
};
use rpki::validation::publication_point::PublicationPointSnapshot;

struct NoopHttpFetcher;
impl Fetcher for NoopHttpFetcher {
    fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
        Err("http disabled in test".to_string())
    }
}

fn cernet_fixture() -> (std::path::PathBuf, String, String) {
    let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
    let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
    let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft".to_string();
    (dir, rsync_base_uri, manifest_file)
}

fn validation_time_from_manifest_fixture(
    dir: &std::path::Path,
    manifest_file: &str,
) -> time::OffsetDateTime {
    let bytes = std::fs::read(dir.join(manifest_file)).expect("read manifest fixture");
    let mft = rpki::data_model::manifest::ManifestObject::decode_der(&bytes).expect("decode mft");
    let this_update = mft.manifest.this_update;
    let next_update = mft.manifest.next_update;
    let candidate = this_update + time::Duration::seconds(60);
    if candidate < next_update {
        candidate
    } else {
        this_update
    }
}

fn issuer_ca_fixture() -> Vec<u8> {
    std::fs::read(
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
            "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 minimal_snapshot(
    manifest_rsync_uri: &str,
    publication_point_rsync_uri: &str,
    manifest_bytes: Vec<u8>,
    files: Vec<PackFile>,
    validation_time: time::OffsetDateTime,
) -> PublicationPointSnapshot {
    // Keep times consistent enough for a valid snapshot.
    PublicationPointSnapshot {
        format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
        manifest_rsync_uri: manifest_rsync_uri.to_string(),
        publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
        manifest_number_be: vec![1],
        this_update: PackTime::from_utc_offset_datetime(validation_time),
        next_update: PackTime::from_utc_offset_datetime(validation_time + time::Duration::hours(1)),
        verified_at: PackTime::from_utc_offset_datetime(validation_time),
        manifest_bytes,
        files,
    }
}

fn build_publication_point_snapshot_from_local_rsync_fixture(
    dir: &std::path::Path,
    rsync_base_uri: &str,
    manifest_rsync_uri: &str,
    validation_time: time::OffsetDateTime,
) -> rpki::validation::publication_point::PublicationPointSnapshot {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy {
        sync_preference: SyncPreference::RsyncOnly,
        ..Policy::default()
    };

    sync_publication_point(
        &store,
        &policy,
        None,
        rsync_base_uri,
        &NoopHttpFetcher,
        &LocalDirRsyncFetcher::new(dir),
        None,
        None,
    )
    .expect("sync into current repository view");

    let pp = process_manifest_publication_point(
        &store,
        &policy,
        manifest_rsync_uri,
        rsync_base_uri,
        issuer_ca_fixture().as_slice(),
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .expect("process manifest");

    pp.snapshot
}

#[test]
fn process_snapshot_for_issuer_extracts_vrps_from_real_cernet_fixture() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);

    let pack = build_publication_point_snapshot_from_local_rsync_fixture(
        &dir,
        &rsync_base_uri,
        &manifest_rsync_uri,
        validation_time,
    );

    let issuer_ca_der = issuer_ca_fixture();
    let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
        .expect("decode issuer ca");

    let policy = Policy::default();
    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
    );

    assert!(
        out.vrps.len() > 10,
        "expected many VRPs, got {}",
        out.vrps.len()
    );
    assert!(out.aspas.is_empty());
}

#[test]
fn parallel_roa_processing_matches_serial_for_real_cernet_fixture() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);

    let pack = build_publication_point_snapshot_from_local_rsync_fixture(
        &dir,
        &rsync_base_uri,
        &manifest_rsync_uri,
        validation_time,
    );

    let issuer_ca_der = issuer_ca_fixture();
    let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
        .expect("decode issuer ca");

    let policy = Policy::default();
    let serial = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
    );
    let parallel = process_publication_point_snapshot_for_issuer_parallel_roa(
        &pack,
        &policy,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
        &ParallelPhase2Config {
            object_workers: 2,
            worker_queue_capacity: 4,
            ..ParallelPhase2Config::default()
        },
    );

    assert_eq!(parallel.vrps, serial.vrps);
    assert_eq!(parallel.aspas, serial.aspas);
    assert_eq!(parallel.local_outputs_cache, serial.local_outputs_cache);
    assert_eq!(parallel.audit, serial.audit);
    assert_eq!(parallel.warnings, serial.warnings);
    assert_eq!(parallel.stats, serial.stats);
}

#[test]
fn parallel_roa_processing_reports_issuer_decode_failure_like_serial() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
    let pack = build_publication_point_snapshot_from_local_rsync_fixture(
        &dir,
        &rsync_base_uri,
        &manifest_rsync_uri,
        validation_time,
    );

    let policy = Policy::default();
    let serial = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &[0, 1, 2],
        Some(issuer_ca_rsync_uri()),
        None,
        None,
        validation_time,
        None,
    );
    let parallel = process_publication_point_snapshot_for_issuer_parallel_roa(
        &pack,
        &policy,
        &[0, 1, 2],
        Some(issuer_ca_rsync_uri()),
        None,
        None,
        validation_time,
        None,
        &ParallelPhase2Config {
            object_workers: 2,
            worker_queue_capacity: 4,
            ..ParallelPhase2Config::default()
        },
    );

    assert_eq!(parallel.vrps, serial.vrps);
    assert_eq!(parallel.audit, serial.audit);
    assert_eq!(parallel.warnings, serial.warnings);
    assert_eq!(parallel.stats, serial.stats);
    assert!(parallel.stats.publication_point_dropped);
}

#[test]
fn parallel_roa_processing_reports_missing_crl_like_serial() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
    let mut pack = build_publication_point_snapshot_from_local_rsync_fixture(
        &dir,
        &rsync_base_uri,
        &manifest_rsync_uri,
        validation_time,
    );
    pack.files.retain(|file| !file.rsync_uri.ends_with(".crl"));

    let issuer_ca_der = issuer_ca_fixture();
    let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
        .expect("decode issuer ca");
    let policy = Policy::default();
    let serial = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
    );
    let parallel = process_publication_point_snapshot_for_issuer_parallel_roa(
        &pack,
        &policy,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
        &ParallelPhase2Config {
            object_workers: 2,
            worker_queue_capacity: 4,
            ..ParallelPhase2Config::default()
        },
    );

    assert_eq!(parallel.vrps, serial.vrps);
    assert_eq!(parallel.audit, serial.audit);
    assert_eq!(parallel.warnings, serial.warnings);
    assert_eq!(parallel.stats, serial.stats);
    assert!(parallel.stats.publication_point_dropped);
}

#[test]
fn signed_object_failure_policy_drop_object_drops_only_bad_object() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);

    let mut pack = build_publication_point_snapshot_from_local_rsync_fixture(
        &dir,
        &rsync_base_uri,
        &manifest_rsync_uri,
        validation_time,
    );

    let roa_uris = pack
        .files
        .iter()
        .filter(|f| f.rsync_uri.ends_with(".roa"))
        .map(|f| f.rsync_uri.clone())
        .collect::<Vec<_>>();

    let bad_idx = pack
        .files
        .iter()
        .position(|f| f.rsync_uri.ends_with(".roa"))
        .expect("snapshot contains roa");
    let bad_uri = pack.files[bad_idx].rsync_uri.clone();
    pack.files[bad_idx] = PackFile::from_bytes_compute_sha256(bad_uri, vec![0u8]);

    let issuer_ca_der = issuer_ca_fixture();
    let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
        .expect("decode issuer ca");

    let policy = Policy {
        signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
        ..Policy::default()
    };
    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
    );

    assert!(out.vrps.len() > 0);
    assert!(!out.warnings.is_empty());
    assert_eq!(
        out.audit.len(),
        roa_uris.len(),
        "expected one audit entry per ROA"
    );
    assert!(
        out.audit
            .iter()
            .any(|e| e.rsync_uri == pack.files[bad_idx].rsync_uri
                && matches!(e.result, rpki::audit::AuditObjectResult::Error)),
        "expected audit error for the corrupted ROA"
    );
}

#[test]
fn signed_object_failure_policy_drop_publication_point_drops_all_output() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);

    let mut pack = build_publication_point_snapshot_from_local_rsync_fixture(
        &dir,
        &rsync_base_uri,
        &manifest_rsync_uri,
        validation_time,
    );

    let roa_uris = pack
        .files
        .iter()
        .filter(|f| f.rsync_uri.ends_with(".roa"))
        .map(|f| f.rsync_uri.clone())
        .collect::<Vec<_>>();

    let bad_idx = pack
        .files
        .iter()
        .position(|f| f.rsync_uri.ends_with(".roa"))
        .expect("snapshot contains roa");
    let bad_uri = pack.files[bad_idx].rsync_uri.clone();
    pack.files[bad_idx] = PackFile::from_bytes_compute_sha256(bad_uri, vec![0u8]);

    let issuer_ca_der = issuer_ca_fixture();
    let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
        .expect("decode issuer ca");

    let policy = Policy {
        signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
        ..Policy::default()
    };
    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
    );

    assert!(out.vrps.is_empty());
    assert!(out.aspas.is_empty());
    assert!(!out.warnings.is_empty());
    assert_eq!(
        out.audit.len(),
        roa_uris.len(),
        "expected audit entries for all ROAs (error + skipped due to policy)"
    );
    let bad_entry = out
        .audit
        .iter()
        .find(|e| e.rsync_uri == pack.files[bad_idx].rsync_uri)
        .expect("bad roa audit entry");
    assert!(
        matches!(bad_entry.result, rpki::audit::AuditObjectResult::Error),
        "expected error for corrupted ROA"
    );
    assert!(
        out.audit
            .iter()
            .any(|e| matches!(e.result, rpki::audit::AuditObjectResult::Skipped)),
        "expected at least one skipped ROA due to drop_publication_point policy"
    );
}

#[test]
fn process_snapshot_for_issuer_without_crl_drops_publication_point() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
    let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");

    let pack = minimal_snapshot(
        &manifest_rsync_uri,
        &rsync_base_uri,
        manifest_bytes,
        vec![
            // include a ROA-looking file; it won't be reached because CRL selection fails first
            PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}dummy.roa"), vec![1]),
        ],
        validation_time,
    );

    let policy = Policy::default();
    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &[],
        None,
        None,
        None,
        validation_time,
        None,
    );

    assert!(out.vrps.is_empty());
    assert!(out.aspas.is_empty());
    assert!(!out.warnings.is_empty());
}

#[test]
fn process_snapshot_for_issuer_handles_invalid_aspa_bytes() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
    let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");
    let crl_bytes =
        std::fs::read(dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl")).expect("read crl");

    let pack = minimal_snapshot(
        &manifest_rsync_uri,
        &rsync_base_uri,
        manifest_bytes,
        vec![
            PackFile::from_bytes_compute_sha256(
                format!("{rsync_base_uri}05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl"),
                crl_bytes,
            ),
            PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}bad.asa"), vec![0u8]),
        ],
        validation_time,
    );

    let policy = Policy {
        signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
        ..Policy::default()
    };
    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &[],
        None,
        None,
        None,
        validation_time,
        None,
    );
    assert!(out.aspas.is_empty());
    assert!(!out.warnings.is_empty());
}

#[test]
fn process_snapshot_for_issuer_drop_publication_point_on_invalid_aspa_bytes() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
    let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");
    let crl_bytes =
        std::fs::read(dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl")).expect("read crl");

    let pack = minimal_snapshot(
        &manifest_rsync_uri,
        &rsync_base_uri,
        manifest_bytes,
        vec![
            PackFile::from_bytes_compute_sha256(
                format!("{rsync_base_uri}05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl"),
                crl_bytes,
            ),
            PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}bad.asa"), vec![0u8]),
            // A ROA-like file: we should not reach it due to policy.
            PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}dummy.roa"), vec![1u8]),
        ],
        validation_time,
    );

    let policy = Policy {
        signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
        ..Policy::default()
    };
    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &[],
        None,
        None,
        None,
        validation_time,
        None,
    );
    assert!(out.vrps.is_empty());
    assert!(out.aspas.is_empty());
    assert!(!out.warnings.is_empty());
}

#[test]
fn process_snapshot_for_issuer_populates_local_outputs_cache_from_real_cernet_fixture() {
    let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
    let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
    let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);

    let pack = build_publication_point_snapshot_from_local_rsync_fixture(
        &dir,
        &rsync_base_uri,
        &manifest_rsync_uri,
        validation_time,
    );

    let issuer_ca_der = issuer_ca_fixture();
    let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
        .expect("decode issuer ca");

    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &Policy::default(),
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        issuer_ca.tbs.extensions.ip_resources.as_ref(),
        issuer_ca.tbs.extensions.as_resources.as_ref(),
        validation_time,
        None,
    );

    assert!(
        !out.local_outputs_cache.is_empty(),
        "expected cached VCIR local outputs"
    );
    assert_eq!(out.local_outputs_cache.len(), out.vrps.len());
    assert!(
        out.local_outputs_cache
            .iter()
            .all(|entry| entry.output_type == VcirOutputType::Vrp)
    );
    assert!(
        out.local_outputs_cache
            .iter()
            .all(|entry| entry.source_object_type == rpki::storage::VcirSourceObjectType::Roa)
    );
}

// NOTE: DN-based issuer resolution and pack-local CA indexing have been removed for determinism.
