use std::path::Path;

use sha2::Digest;

use rpki::data_model::crl::RpkixCrl;
use rpki::data_model::manifest::ManifestObject;
use rpki::data_model::rc::ResourceCertificate;
use rpki::policy::{Policy, SignedObjectFailurePolicy};
use rpki::storage::{
    PackFile, RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore,
};
use rpki::validation::manifest::process_manifest_publication_point;
use rpki::validation::objects::process_publication_point_snapshot_for_issuer;

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 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 build_cernet_pack_and_validation_time() -> (
    rpki::validation::publication_point::PublicationPointSnapshot,
    time::OffsetDateTime,
    Vec<u8>,
    ResourceCertificate,
) {
    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 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");

    put_current_object(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft");
    let entries = manifest
        .manifest
        .parse_files()
        .expect("parse validated manifest fileList");
    for entry in &entries {
        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);
        let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin");
        put_current_object(&store, &rsync_uri, bytes, object_type);
    }

    let issuer_ca_der = std::fs::read(
        "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
    )
    .expect("read issuer CA cert fixture");

    let policy = Policy::default();
    let out = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        &publication_point_rsync_uri,
        &issuer_ca_der,
        Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"),
        manifest.manifest.this_update + time::Duration::seconds(1),
    )
    .expect("process manifest publication point");
    let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer CA cert");

    let crl_file = out
        .snapshot
        .files
        .iter()
        .find(|f| f.rsync_uri.ends_with(".crl"))
        .expect("crl present in snapshot");
    let crl = RpkixCrl::decode_der(crl_file.bytes().expect("load crl bytes")).expect("decode crl");

    let mut t = manifest.manifest.this_update;
    if issuer_ca.tbs.validity_not_before > t {
        t = issuer_ca.tbs.validity_not_before;
    }
    if crl.this_update.utc > t {
        t = crl.this_update.utc;
    }
    t += time::Duration::seconds(1);

    (out.snapshot, t, issuer_ca_der, issuer_ca)
}

#[test]
fn missing_crl_causes_roas_to_be_dropped_under_drop_object_policy() {
    let (mut pack, validation_time, issuer_ca_der, issuer_ca) =
        build_cernet_pack_and_validation_time();
    pack.files.retain(|f| !f.rsync_uri.ends_with(".crl"));

    let mut policy = Policy::default();
    policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;

    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &issuer_ca_der,
        None,
        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.warnings.is_empty());
    assert!(out.stats.publication_point_dropped);
}

#[test]
fn wrong_issuer_ca_cert_causes_roas_to_be_dropped_under_drop_object_policy() {
    let (pack, validation_time, _issuer_ca_der, _issuer_ca) =
        build_cernet_pack_and_validation_time();

    let mut policy = Policy::default();
    policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;

    // Use an unrelated trust anchor certificate as the issuer to force EE cert path validation to fail.
    let wrong_issuer_ca_der =
        std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read wrong issuer ca");
    let out = process_publication_point_snapshot_for_issuer(
        &pack,
        &policy,
        &wrong_issuer_ca_der,
        None,
        None,
        None,
        validation_time,
        None,
    );
    assert!(out.vrps.is_empty());
    assert!(!out.warnings.is_empty());
}

#[test]
fn invalid_aspa_object_is_reported_as_warning_under_drop_object_policy() {
    let (mut pack, validation_time, issuer_ca_der, issuer_ca) =
        build_cernet_pack_and_validation_time();

    let uri = "rsync://rpki.cernet.net/repo/cernet/0/INVALID.asa".to_string();
    pack.files.push(PackFile::from_bytes_compute_sha256(
        uri.clone(),
        b"\0\0".to_vec(),
    ));

    let mut policy = Policy::default();
    policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;

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

    assert!(
        out.warnings
            .iter()
            .any(|w| w.context.as_deref() == Some(&uri)),
        "expected warning for invalid ASPA"
    );
}
