use rpki::ccr::{CcrAccumulator, build_roa_payload_state};
use rpki::data_model::roa::{IpPrefix, RoaAfi};
use rpki::storage::{PackTime, VcirCcrManifestProjection};
use rpki::validation::objects::Vrp;

const MANIFEST_URI: &str = "rsync://example.test/repo/current.mft";
const OID_AD_SIGNED_OBJECT: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x0b];
const OID_AD_RPKI_NOTIFY: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x0d];

fn der_length(len: usize) -> Vec<u8> {
    if len < 0x80 {
        return vec![len as u8];
    }
    let mut bytes = len.to_be_bytes().to_vec();
    let first = bytes
        .iter()
        .position(|byte| *byte != 0)
        .expect("non-zero length");
    bytes.drain(..first);
    let mut out = vec![0x80 | bytes.len() as u8];
    out.extend_from_slice(&bytes);
    out
}

fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
    let mut out = vec![tag];
    out.extend(der_length(value.len()));
    out.extend_from_slice(value);
    out
}

fn access_description_der(access_method_oid: &[u8], uri: &str) -> Vec<u8> {
    let oid = der_tlv(0x06, access_method_oid);
    let location = der_tlv(0x86, uri.as_bytes());
    let mut body = oid;
    body.extend(location);
    der_tlv(0x30, &body)
}

fn projection(locations: Vec<Vec<u8>>) -> VcirCcrManifestProjection {
    VcirCcrManifestProjection {
        manifest_rsync_uri: MANIFEST_URI.to_string(),
        manifest_sha256: vec![0x11; 32],
        manifest_size: 1000,
        manifest_ee_aki: vec![0x22; 20],
        manifest_number_be: vec![1],
        manifest_this_update: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
        manifest_sia_locations_der: locations,
        subordinate_skis: vec![vec![0x33; 20]],
    }
}

#[test]
fn historical_projection_normalizes_locations_and_accounts_memory() {
    let signed_object = access_description_der(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
    let notify = access_description_der(
        OID_AD_RPKI_NOTIFY,
        "https://rrdp.example.test/notification.xml",
    );
    let projection = projection(vec![notify, signed_object]);

    let mut accumulator = CcrAccumulator::new(Vec::new());
    accumulator
        .append_manifest_projection(&projection)
        .expect("append normalized historical projection");
    accumulator
        .append_manifest_projection(&projection)
        .expect("duplicate matching projection is idempotent");

    assert_eq!(accumulator.manifest_count(), 1);
    let stats = accumulator.memory_stats();
    assert_eq!(stats.manifest_count, 1);
    assert_eq!(stats.locations_der_count, 1);
    assert_eq!(stats.subordinate_ski_count, 1);
    assert!(stats.estimated_heap_bytes > 0);
    assert!(stats.btree_entry_shallow_bytes > 0);

    let mut conflicting = projection;
    conflicting.subordinate_skis.push(vec![0x44; 20]);
    let error = accumulator
        .append_manifest_projection(&conflicting)
        .expect_err("same hash with different projection must fail");
    assert!(error.contains("duplicate manifest hash"), "{error}");
}

#[test]
fn historical_projection_rejects_invalid_location_sets() {
    let mut accumulator = CcrAccumulator::new(Vec::new());

    let empty_error = accumulator
        .append_manifest_projection(&projection(Vec::new()))
        .expect_err("empty location set must fail");
    assert!(empty_error.contains("contains 0"), "{empty_error}");

    let signed_object = access_description_der(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
    let duplicate_error = accumulator
        .append_manifest_projection(&projection(vec![signed_object.clone(), signed_object]))
        .expect_err("duplicate matching locations must fail");
    assert!(duplicate_error.contains("contains 2"), "{duplicate_error}");

    let malformed_error = accumulator
        .append_manifest_projection(&projection(vec![vec![0x30, 0x01, 0x06]]))
        .expect_err("malformed location DER must fail");
    assert!(
        malformed_error.contains("truncated DER"),
        "{malformed_error}"
    );
}

#[test]
fn roa_payload_builder_encodes_large_non_byte_aligned_family() {
    let mut vrps = Vec::new();
    for octet in 0..=255 {
        vrps.push(Vrp {
            asn: 64_496,
            prefix: IpPrefix {
                afi: RoaAfi::Ipv4,
                prefix_len: 24,
                addr: [10, octet, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            },
            max_length: 24,
        });
    }
    vrps.push(Vrp {
        asn: 64_496,
        prefix: IpPrefix {
            afi: RoaAfi::Ipv4,
            prefix_len: 13,
            addr: [10, 0x1f, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        },
        max_length: 24,
    });

    let state = build_roa_payload_state(&vrps).expect("build ROA payload state");
    assert_eq!(state.rps.len(), 1);
    assert_eq!(state.rps[0].as_id, 64_496);
    assert_eq!(state.rps[0].ip_addr_blocks.len(), 1);
    assert!(state.rps[0].ip_addr_blocks[0].len() > 128);
}
