use std::collections::HashMap;

use rpki::storage::RocksStore;
use rpki::sync::rrdp::{Fetcher, parse_notification_snapshot, sync_from_notification_snapshot};
use sha2::Digest;

struct MapFetcher {
    by_uri: HashMap<String, Vec<u8>>,
}

impl Fetcher for MapFetcher {
    fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
        self.by_uri
            .get(uri)
            .cloned()
            .ok_or_else(|| format!("not found: {uri}"))
    }
}

fn assert_current_object(store: &RocksStore, uri: &str, expected: &[u8]) {
    assert_eq!(
        store
            .load_current_object_bytes_by_uri(uri)
            .expect("load current object"),
        Some(expected.to_vec())
    );
}

#[test]
fn notification_parses_and_snapshot_is_applied_and_state_written() {
    let notification_xml =
        std::fs::read("tests/fixtures/rrdp/notification.xml").expect("read notification");
    let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");

    let notif = parse_notification_snapshot(&notification_xml).expect("parse notification");
    assert_eq!(notif.serial, 1);
    assert_eq!(notif.snapshot_uri, "https://example.net/rrdp/snapshot.xml");

    let mut fetcher = MapFetcher {
        by_uri: HashMap::new(),
    };
    fetcher.by_uri.insert(
        "https://example.net/rrdp/snapshot.xml".to_string(),
        snapshot_xml,
    );

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");

    let published = sync_from_notification_snapshot(
        &store,
        "https://example.net/rrdp/notification.xml",
        &notification_xml,
        &fetcher,
    )
    .expect("sync");
    assert_eq!(published, 2);

    assert_current_object(&store, "rsync://example.net/repo/obj1.cer", b"abc");
    assert_current_object(&store, "rsync://example.net/repo/obj2.crl", b"def");

    let source = store
        .get_rrdp_source_record("https://example.net/rrdp/notification.xml")
        .expect("get source")
        .expect("source exists");
    assert_eq!(
        source.last_session_id.expect("session id"),
        "9df4b597-af9e-4dca-bdda-719cce2c4e28".to_string()
    );
    assert_eq!(source.last_serial, Some(1));
}

#[test]
fn snapshot_hash_mismatch_is_rejected() {
    let mut notification_xml =
        String::from_utf8(std::fs::read("tests/fixtures/rrdp/notification.xml").unwrap()).unwrap();
    notification_xml = notification_xml.replace(
        "dcb1ce91401d568d7ddf7a4c9f70c65d8428c3a5e7135f82db99c4de30413551",
        "0000000000000000000000000000000000000000000000000000000000000000",
    );

    let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");
    let fetcher = MapFetcher {
        by_uri: HashMap::from([(
            "https://example.net/rrdp/snapshot.xml".to_string(),
            snapshot_xml,
        )]),
    };

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    let err = sync_from_notification_snapshot(
        &store,
        "https://example.net/rrdp/notification.xml",
        notification_xml.as_bytes(),
        &fetcher,
    )
    .expect_err("hash mismatch rejected");
    assert!(err.to_string().contains("hash mismatch"));
}

#[test]
fn session_id_mismatch_is_rejected() {
    let notification_xml =
        std::fs::read("tests/fixtures/rrdp/notification.xml").expect("read notification");
    let mut snapshot_xml =
        String::from_utf8(std::fs::read("tests/fixtures/rrdp/snapshot.xml").unwrap()).unwrap();
    snapshot_xml = snapshot_xml.replace(
        "9df4b597-af9e-4dca-bdda-719cce2c4e28",
        "00000000-0000-4000-8000-000000000000",
    );
    let snapshot_xml = snapshot_xml.into_bytes();

    // Recompute snapshot hash and patch notification to keep hash correct, so we test
    // the session_id mismatch check.
    let mut notif_str = String::from_utf8(notification_xml).unwrap();
    let digest = sha2::Sha256::digest(&snapshot_xml);
    let hex_hash = hex::encode(digest);
    notif_str = notif_str.replace(
        "dcb1ce91401d568d7ddf7a4c9f70c65d8428c3a5e7135f82db99c4de30413551",
        &hex_hash,
    );

    let fetcher = MapFetcher {
        by_uri: HashMap::from([(
            "https://example.net/rrdp/snapshot.xml".to_string(),
            snapshot_xml,
        )]),
    };

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    let err = sync_from_notification_snapshot(
        &store,
        "https://example.net/rrdp/notification.xml",
        notif_str.as_bytes(),
        &fetcher,
    )
    .expect_err("session_id mismatch rejected");

    assert!(err.to_string().contains("session_id mismatch"));
}

#[test]
fn serial_zero_is_rejected() {
    let notification_xml =
        String::from_utf8(std::fs::read("tests/fixtures/rrdp/notification.xml").unwrap()).unwrap();
    let notification_xml = notification_xml.replace("serial=\"1\"", "serial=\"0\"");

    let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");
    let fetcher = MapFetcher {
        by_uri: HashMap::from([(
            "https://example.net/rrdp/snapshot.xml".to_string(),
            snapshot_xml,
        )]),
    };

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    let err = sync_from_notification_snapshot(
        &store,
        "https://example.net/rrdp/notification.xml",
        notification_xml.as_bytes(),
        &fetcher,
    )
    .expect_err("serial=0 rejected");
    assert!(err.to_string().contains("serial invalid"));
}
