use std::path::Path;

use sha2::Digest;

use rpki::data_model::manifest::ManifestObject;
use rpki::policy::Policy;
use rpki::storage::{RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore};
use rpki::validation::manifest::{
    ManifestProcessError, PublicationPointSource, process_manifest_publication_point,
};

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 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");
}

#[test]
fn manifest_outside_publication_point_yields_no_usable_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 its locked files so Fresh would otherwise succeed.
    put_current_object(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft");
    for entry in manifest
        .manifest
        .parse_files()
        .expect("parse fileList")
        .iter()
    {
        let file_path = manifest_path
            .parent()
            .unwrap()
            .join(entry.file_name.as_str());
        let bytes = std::fs::read(&file_path).expect("read referenced file");
        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"),
        );
    }

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

    // Pass a publication point URI that does not include the manifest URI.
    let wrong_pp = "rsync://example.test/not-the-pp/";
    let err = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        wrong_pp,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .unwrap_err();

    // With no reusable current-instance snapshot/VCIR for this wrong publication point, we get NoUsableCache.
    assert!(
        matches!(err, ManifestProcessError::NoUsableCache { .. }),
        "{err}"
    );
}

#[test]
fn manifest_outside_publication_point_detects_current_instance_snapshot_pp_mismatch() {
    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");

    put_current_object(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft");
    for entry in manifest
        .manifest
        .parse_files()
        .expect("parse fileList")
        .iter()
    {
        let file_path = manifest_path
            .parent()
            .unwrap()
            .join(entry.file_name.as_str());
        let bytes = std::fs::read(&file_path).expect("read referenced file");
        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"),
        );
    }

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

    // First run validates a fresh publication point snapshot and stores the latest VCIR.
    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);

    // Second run with wrong publication point: fresh fails outside PP; cache load also fails
    // because the reusable current-instance snapshot carries a different publication_point_rsync_uri.
    let wrong_pp = "rsync://example.test/not-the-pp/";
    let err = process_manifest_publication_point(
        &store,
        &policy,
        &manifest_rsync_uri,
        wrong_pp,
        &issuer_ca_der,
        Some(issuer_ca_rsync_uri()),
        validation_time,
    )
    .unwrap_err();
    assert!(
        matches!(err, ManifestProcessError::NoUsableCache { .. }),
        "{err}"
    );
}
