use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::Path;

use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};

use crate::data_model::aspa::AspaObject;
use crate::data_model::crl::RpkixCrl;
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::{
    AccessDescription, RcExtensions, ResourceCertificate, SubjectInfoAccess,
};
use crate::data_model::roa::{IpPrefix as RoaIpPrefix, RoaAfi, RoaObject};
use crate::data_model::signed_object::{
    ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignerInfoProfiled,
};
use crate::data_model::ta::TaCertificate;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ObjectType {
    Auto,
    Cer,
    Mft,
    Crl,
    Roa,
    Aspa,
}

impl ObjectType {
    pub fn parse(value: &str) -> Result<Self, String> {
        match value.to_ascii_lowercase().as_str() {
            "auto" => Ok(Self::Auto),
            "cer" | ".cer" | "cert" | "certificate" => Ok(Self::Cer),
            "mft" | ".mft" | "manifest" => Ok(Self::Mft),
            "crl" | ".crl" => Ok(Self::Crl),
            "roa" | ".roa" => Ok(Self::Roa),
            "asa" | ".asa" | "aspa" => Ok(Self::Aspa),
            _ => Err(format!("unsupported object type: {value}")),
        }
    }

    pub fn label(self) -> &'static str {
        object_type_label(self)
    }
}

pub fn resolve_object_type(object_type: ObjectType, path: &Path) -> Result<ObjectType, String> {
    if object_type != ObjectType::Auto {
        return Ok(object_type);
    }
    match path
        .extension()
        .and_then(|v| v.to_str())
        .map(|v| v.to_ascii_lowercase())
        .as_deref()
    {
        Some("cer") => Ok(ObjectType::Cer),
        Some("mft") => Ok(ObjectType::Mft),
        Some("crl") => Ok(ObjectType::Crl),
        Some("roa") => Ok(ObjectType::Roa),
        Some("asa") | Some("aspa") => Ok(ObjectType::Aspa),
        _ => Err(format!(
            "cannot infer object type from path: {}",
            path.display()
        )),
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ObjectProjectionRecord {
    pub schema_version: u32,
    pub sha256: String,
    pub object_type: String,
    pub parse_status: String,
    pub error_summary: Option<String>,
    pub projection: Value,
}

pub fn build_object_projection(
    object_type: ObjectType,
    input_path: &Path,
    bytes: &[u8],
    entry_limit: usize,
) -> ObjectProjectionRecord {
    let resolved = match resolve_object_type(object_type, input_path) {
        Ok(value) => value,
        Err(err) => {
            return ObjectProjectionRecord {
                schema_version: 1,
                sha256: sha256_hex(bytes),
                object_type: "unknown".to_string(),
                parse_status: "error".to_string(),
                error_summary: Some(err),
                projection: json!({"decode": {"profileValid": false}}),
            };
        }
    };
    let projection = parse_object_json(resolved, input_path, bytes, entry_limit);
    let parse_status = if projection
        .get("object")
        .and_then(|v| v.get("decode"))
        .and_then(|v| v.get("profileValid"))
        .and_then(Value::as_bool)
        .unwrap_or(false)
    {
        "ok"
    } else {
        "error"
    };
    let error_summary = projection
        .get("object")
        .and_then(|v| v.get("decode"))
        .and_then(|v| v.get("error"))
        .and_then(Value::as_str)
        .map(str::to_string);
    ObjectProjectionRecord {
        schema_version: 1,
        sha256: sha256_hex(bytes),
        object_type: resolved.label().to_string(),
        parse_status: parse_status.to_string(),
        error_summary,
        projection,
    }
}

pub fn parse_object_json(
    object_type: ObjectType,
    input_path: &Path,
    bytes: &[u8],
    entry_limit: usize,
) -> Value {
    let object = match object_type {
        ObjectType::Auto => unreachable!("auto must be resolved"),
        ObjectType::Cer => parse_cer_json(bytes),
        ObjectType::Mft => parse_mft_json(bytes, entry_limit),
        ObjectType::Crl => parse_crl_json(bytes, entry_limit),
        ObjectType::Roa => parse_roa_json(bytes, entry_limit),
        ObjectType::Aspa => parse_aspa_json(bytes, entry_limit),
    };
    json!({
        "tool": "rpki_object_parse",
        "schemaVersion": 1,
        "input": {
            "path": input_path.display().to_string(),
            "type": object_type_label(object_type),
            "bytes": bytes_summary(bytes),
        },
        "object": object,
    })
}

pub fn parse_cer_json(bytes: &[u8]) -> Value {
    match ResourceCertificate::decode_der(bytes) {
        Ok(cert) => {
            let ta_profile = match TaCertificate::decode_der(bytes) {
                Ok(ta) => json!({
                    "valid": true,
                    "selfSignature": result_json(ta.verify_self_signature().map_err(|e| e.to_string())),
                }),
                Err(err) => json!({
                    "valid": false,
                    "error": err.to_string(),
                }),
            };
            json!({
                "type": "cer",
                "decode": {"profileValid": true},
                "resourceCertificate": resource_certificate_json(&cert),
                "trustAnchorProfile": ta_profile,
            })
        }
        Err(err) => json!({
            "type": "cer",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

pub fn parse_mft_json(bytes: &[u8], entry_limit: usize) -> Value {
    match ManifestObject::decode_der(bytes) {
        Ok(mft) => {
            let files = mft.manifest.parse_files();
            let (file_sample, file_list_error) = match files {
                Ok(entries) => (
                    json!({
                        "count": entries.len(),
                        "truncated": entries.len() > entry_limit,
                        "entries": entries.iter().take(entry_limit).map(|item| {
                            json!({"fileName": item.file_name, "hashHex": hex::encode(item.hash_bytes)})
                        }).collect::<Vec<_>>(),
                    }),
                    Value::Null,
                ),
                Err(err) => (Value::Null, json!(err.to_string())),
            };
            json!({
                "type": "mft",
                "decode": {"profileValid": true},
                "eContentType": mft.econtent_type,
                "signedObject": signed_object_json(&mft.signed_object),
                "manifest": {
                    "version": mft.manifest.version,
                    "manifestNumberHex": mft.manifest.manifest_number.to_hex_upper(),
                    "thisUpdate": format_time(mft.manifest.this_update),
                    "nextUpdate": format_time(mft.manifest.next_update),
                    "fileHashAlg": mft.manifest.file_hash_alg,
                    "fileCount": mft.manifest.file_count(),
                    "fileList": file_sample,
                    "fileListError": file_list_error,
                },
                "embeddedEeProfile": result_json(mft.validate_embedded_ee_cert().map_err(|e| e.to_string())),
                "cmsSignature": result_json(mft.signed_object.verify_signature().map_err(|e| e.to_string())),
            })
        }
        Err(err) => json!({
            "type": "mft",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

pub fn parse_crl_json(bytes: &[u8], entry_limit: usize) -> Value {
    match RpkixCrl::decode_der(bytes) {
        Ok(crl) => json!({
            "type": "crl",
            "decode": {"profileValid": true},
            "rawDer": bytes_summary(&crl.raw_der),
            "version": crl.version,
            "issuer": crl.issuer_dn,
            "signatureAlgorithm": crl.signature_algorithm_oid,
            "thisUpdate": format_time(crl.this_update.utc),
            "nextUpdate": format_time(crl.next_update.utc),
            "extensions": {
                "authorityKeyIdentifier": hex::encode(&crl.extensions.authority_key_identifier),
                "crlNumberHex": crl.extensions.crl_number.to_hex_upper(),
                "crlNumber": crl.extensions.crl_number.to_u64(),
            },
            "revokedCertificates": {
                "count": crl.revoked_certs.len(),
                "truncated": crl.revoked_certs.len() > entry_limit,
                "entries": crl.revoked_certs.iter().take(entry_limit).map(|item| {
                    json!({
                        "serialNumberHex": item.serial_number.to_hex_upper(),
                        "serialNumber": item.serial_number.to_u64(),
                        "revocationDate": format_time(item.revocation_date.utc),
                    })
                }).collect::<Vec<_>>(),
            },
        }),
        Err(err) => json!({
            "type": "crl",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

pub fn manifest_file_entries_page(
    bytes: &[u8],
    offset: usize,
    limit: usize,
) -> Result<(usize, Vec<Value>), String> {
    let mft = ManifestObject::decode_der(bytes).map_err(|err| err.to_string())?;
    let entries = mft.manifest.parse_files().map_err(|err| err.to_string())?;
    let total = entries.len();
    let end = (offset + limit).min(total);
    let page = entries[offset.min(total)..end]
        .iter()
        .map(|item| json!({"fileName": item.file_name, "hashHex": hex::encode(&item.hash_bytes)}))
        .collect::<Vec<_>>();
    Ok((total, page))
}

pub fn crl_revoked_entries_page(
    bytes: &[u8],
    offset: usize,
    limit: usize,
) -> Result<(usize, Vec<Value>), String> {
    let crl = RpkixCrl::decode_der(bytes).map_err(|err| err.to_string())?;
    let total = crl.revoked_certs.len();
    let end = (offset + limit).min(total);
    let page = crl.revoked_certs[offset.min(total)..end]
        .iter()
        .map(|item| {
            json!({
                "serialNumberHex": item.serial_number.to_hex_upper(),
                "serialNumber": item.serial_number.to_u64(),
                "revocationDate": format_time(item.revocation_date.utc),
            })
        })
        .collect::<Vec<_>>();
    Ok((total, page))
}

pub fn parse_roa_json(bytes: &[u8], entry_limit: usize) -> Value {
    match RoaObject::decode_der(bytes) {
        Ok(roa) => json!({
            "type": "roa",
            "decode": {"profileValid": true},
            "eContentType": roa.econtent_type,
            "signedObject": signed_object_json(&roa.signed_object),
            "roa": {
                "version": roa.roa.version,
                "asId": roa.roa.as_id,
                "ipAddressFamilies": roa.roa.ip_addr_blocks.iter().map(|family| {
                    json!({
                        "afi": format!("{:?}", family.afi),
                        "addressCount": family.addresses.len(),
                        "truncated": family.addresses.len() > entry_limit,
                        "addresses": family.addresses.iter().take(entry_limit).map(|entry| {
                            json!({
                                "prefix": roa_prefix_string(&entry.prefix),
                                "maxLength": entry.max_length,
                            })
                        }).collect::<Vec<_>>(),
                    })
                }).collect::<Vec<_>>(),
            },
            "embeddedEeProfile": result_json(roa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
            "cmsSignature": result_json(roa.signed_object.verify_signature().map_err(|e| e.to_string())),
        }),
        Err(err) => json!({
            "type": "roa",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

pub fn parse_aspa_json(bytes: &[u8], entry_limit: usize) -> Value {
    match AspaObject::decode_der(bytes) {
        Ok(aspa) => json!({
            "type": "aspa",
            "decode": {"profileValid": true},
            "eContentType": aspa.econtent_type,
            "signedObject": signed_object_json(&aspa.signed_object),
            "aspa": {
                "version": aspa.aspa.version,
                "customerAsId": aspa.aspa.customer_as_id,
                "providerCount": aspa.aspa.provider_as_ids.len(),
                "providersTruncated": aspa.aspa.provider_as_ids.len() > entry_limit,
                "providerAsIds": aspa.aspa.provider_as_ids.iter().take(entry_limit).copied().collect::<Vec<_>>(),
            },
            "embeddedEeProfile": result_json(aspa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
            "cmsSignature": result_json(aspa.signed_object.verify_signature().map_err(|e| e.to_string())),
        }),
        Err(err) => json!({
            "type": "aspa",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

fn resource_certificate_json(cert: &ResourceCertificate) -> Value {
    let tbs = &cert.tbs;
    json!({
        "rawDer": bytes_summary(&cert.raw_der),
        "kind": format!("{:?}", cert.kind),
        "version": tbs.version,
        "serialNumberHex": hex::encode(tbs.serial_number.to_bytes_be()),
        "signatureAlgorithm": tbs.signature_algorithm,
        "issuer": tbs.issuer_name.to_string(),
        "subject": tbs.subject_name.to_string(),
        "validity": {
            "notBefore": format_time(tbs.validity_not_before),
            "notAfter": format_time(tbs.validity_not_after),
        },
        "subjectPublicKeyInfo": bytes_summary(&tbs.subject_public_key_info),
        "extensions": rc_extensions_json(&tbs.extensions),
    })
}

fn rc_extensions_json(ext: &RcExtensions) -> Value {
    json!({
        "basicConstraintsCa": ext.basic_constraints_ca,
        "subjectKeyIdentifier": ext.subject_key_identifier.as_ref().map(|v| hex::encode(v)),
        "authorityKeyIdentifier": ext.authority_key_identifier.as_ref().map(|v| hex::encode(v)),
        "crlDistributionPointsUris": ext.crl_distribution_points_uris,
        "caIssuersUris": ext.ca_issuers_uris,
        "subjectInfoAccess": subject_info_access_json(ext.subject_info_access.as_ref()),
        "certificatePoliciesOid": ext.certificate_policies_oid,
        "ipResources": serde_json::to_value(&ext.ip_resources).unwrap_or(Value::Null),
        "asResources": serde_json::to_value(&ext.as_resources).unwrap_or(Value::Null),
    })
}

fn subject_info_access_json(value: Option<&SubjectInfoAccess>) -> Value {
    match value {
        None => Value::Null,
        Some(SubjectInfoAccess::Ca(ca)) => json!({
            "kind": "ca",
            "accessDescriptions": ca.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
        }),
        Some(SubjectInfoAccess::Ee(ee)) => json!({
            "kind": "ee",
            "signedObjectUris": ee.signed_object_uris,
            "accessDescriptions": ee.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
        }),
    }
}

fn access_description_json(value: &AccessDescription) -> Value {
    json!({
        "accessMethodOid": value.access_method_oid,
        "accessLocation": value.access_location,
    })
}

fn signed_object_json(signed_object: &RpkiSignedObject) -> Value {
    let signed_data = &signed_object.signed_data;
    json!({
        "rawDer": bytes_summary(&signed_object.raw_der),
        "contentInfoContentType": signed_object.content_info_content_type,
        "signedData": {
            "version": signed_data.version,
            "digestAlgorithms": signed_data.digest_algorithms,
            "encapContentInfo": {
                "eContentType": signed_data.encap_content_info.econtent_type,
                "eContent": bytes_summary(&signed_data.encap_content_info.econtent),
            },
            "certificates": signed_data.certificates.iter().map(ee_certificate_json).collect::<Vec<_>>(),
            "crlsPresent": signed_data.crls_present,
            "signerInfos": signed_data.signer_infos.iter().map(signer_info_json).collect::<Vec<_>>(),
        },
    })
}

fn ee_certificate_json(cert: &ResourceEeCertificate) -> Value {
    json!({
        "rawDer": bytes_summary(&cert.raw_der),
        "subjectKeyIdentifier": hex::encode(&cert.subject_key_identifier),
        "spkiDer": bytes_summary(&cert.spki_der),
        "rsaPublicKey": {
            "modulus": bytes_summary(&cert.rsa_public_modulus),
            "exponent": bytes_summary(&cert.rsa_public_exponent),
        },
        "tbsCertificate": bytes_summary(&cert.tbs_certificate_der),
        "certificateSignature": bytes_summary(&cert.signature_bytes),
        "keyUsageSummary": format!("{:?}", cert.key_usage_summary),
        "siaSignedObjectUris": cert.sia_signed_object_uris,
        "resourceCertificate": resource_certificate_json(&cert.resource_cert),
    })
}

fn signer_info_json(info: &SignerInfoProfiled) -> Value {
    json!({
        "version": info.version,
        "sidSki": hex::encode(&info.sid_ski),
        "digestAlgorithm": info.digest_algorithm,
        "signatureAlgorithm": info.signature_algorithm,
        "signedAttrs": signed_attrs_json(&info.signed_attrs),
        "unsignedAttrsPresent": info.unsigned_attrs_present,
        "signature": bytes_summary(&info.signature),
        "signedAttrsDerForSignature": bytes_summary(&info.signed_attrs_der_for_signature),
    })
}

fn signed_attrs_json(attrs: &SignedAttrsProfiled) -> Value {
    json!({
        "contentType": attrs.content_type,
        "messageDigest": hex::encode(&attrs.message_digest),
        "signingTime": {
            "utc": format_time(attrs.signing_time.utc),
            "encoding": format!("{:?}", attrs.signing_time.encoding),
        },
        "otherAttrsPresent": attrs.other_attrs_present,
    })
}

fn result_json(result: Result<(), String>) -> Value {
    match result {
        Ok(()) => json!({"valid": true}),
        Err(err) => json!({"valid": false, "error": err}),
    }
}

fn object_type_label(object_type: ObjectType) -> &'static str {
    match object_type {
        ObjectType::Auto => "auto",
        ObjectType::Cer => "cer",
        ObjectType::Mft => "mft",
        ObjectType::Crl => "crl",
        ObjectType::Roa => "roa",
        ObjectType::Aspa => "aspa",
    }
}

fn bytes_summary(bytes: &[u8]) -> Value {
    let head_len = bytes.len().min(16);
    let tail_len = bytes.len().min(16);
    json!({
        "len": bytes.len(),
        "sha256": sha256_hex(bytes),
        "headHex": hex::encode(&bytes[..head_len]),
        "tailHex": hex::encode(&bytes[bytes.len().saturating_sub(tail_len)..]),
    })
}

fn sha256_hex(bytes: &[u8]) -> String {
    hex::encode(Sha256::digest(bytes))
}

fn format_time(value: time::OffsetDateTime) -> String {
    value
        .to_offset(time::UtcOffset::UTC)
        .format(&time::format_description::well_known::Rfc3339)
        .unwrap_or_else(|_| value.unix_timestamp().to_string())
}

fn roa_prefix_string(prefix: &RoaIpPrefix) -> String {
    let bytes = prefix.addr_bytes();
    match prefix.afi {
        RoaAfi::Ipv4 => {
            let octets = [bytes[0], bytes[1], bytes[2], bytes[3]];
            format!("{}/{}", Ipv4Addr::from(octets), prefix.prefix_len)
        }
        RoaAfi::Ipv6 => {
            let mut octets = [0u8; 16];
            octets.copy_from_slice(bytes);
            format!("{}/{}", Ipv6Addr::from(octets), prefix.prefix_len)
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::*;

    #[test]
    fn object_type_parser_and_resolver_cover_aliases() {
        assert_eq!(ObjectType::parse("auto").unwrap(), ObjectType::Auto);
        assert_eq!(ObjectType::parse(".cer").unwrap(), ObjectType::Cer);
        assert_eq!(ObjectType::parse("certificate").unwrap(), ObjectType::Cer);
        assert_eq!(ObjectType::parse("manifest").unwrap(), ObjectType::Mft);
        assert_eq!(ObjectType::parse(".crl").unwrap(), ObjectType::Crl);
        assert_eq!(ObjectType::parse("roa").unwrap(), ObjectType::Roa);
        assert_eq!(ObjectType::parse("aspa").unwrap(), ObjectType::Aspa);
        assert_eq!(ObjectType::parse(".asa").unwrap(), ObjectType::Aspa);
        assert!(ObjectType::parse("unknown").is_err());
        assert_eq!(ObjectType::Aspa.label(), "aspa");

        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("repo/a.cer")).unwrap(),
            ObjectType::Cer
        );
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("repo/a.mft")).unwrap(),
            ObjectType::Mft
        );
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("repo/a.crl")).unwrap(),
            ObjectType::Crl
        );
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("repo/a.roa")).unwrap(),
            ObjectType::Roa
        );
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("repo/a.asa")).unwrap(),
            ObjectType::Aspa
        );
        assert_eq!(
            resolve_object_type(ObjectType::Roa, Path::new("repo/a.bin")).unwrap(),
            ObjectType::Roa
        );
        assert!(resolve_object_type(ObjectType::Auto, Path::new("repo/a.bin")).is_err());
    }

    #[test]
    fn invalid_der_returns_error_projection_for_all_object_types() {
        let bytes = b"not der";
        for object_type in [
            ObjectType::Cer,
            ObjectType::Mft,
            ObjectType::Crl,
            ObjectType::Roa,
            ObjectType::Aspa,
        ] {
            let value = parse_object_json(object_type, Path::new("bad.der"), bytes, 1);
            assert_eq!(
                value["object"]["decode"]["profileValid"].as_bool(),
                Some(false)
            );
            assert!(value["object"]["decode"]["error"].as_str().is_some());
        }

        let record = build_object_projection(ObjectType::Auto, Path::new("bad.bin"), bytes, 1);
        assert_eq!(record.object_type, "unknown");
        assert_eq!(record.parse_status, "error");
        assert!(record.error_summary.is_some());
    }

    #[test]
    fn parses_fixture_objects_into_human_readable_projection() {
        let cases = [
            (
                ObjectType::Cer,
                "tests/fixtures/ta/apnic-ta.cer",
                "cer",
                "resourceCertificate",
            ),
            (
                ObjectType::Mft,
                "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
                "mft",
                "manifest",
            ),
            (
                ObjectType::Crl,
                "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
                "crl",
                "revokedCertificates",
            ),
            (
                ObjectType::Roa,
                "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
                "roa",
                "roa",
            ),
            (
                ObjectType::Aspa,
                "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
                "aspa",
                "aspa",
            ),
        ];

        for (object_type, path, expected_type, expected_section) in cases {
            let bytes = std::fs::read(path).expect("fixture");
            let record = build_object_projection(object_type, Path::new(path), &bytes, 1);
            assert_eq!(record.object_type, expected_type);
            assert_eq!(record.parse_status, "ok");
            assert_eq!(
                record.projection["object"]["decode"]["profileValid"].as_bool(),
                Some(true)
            );
            assert!(record.projection["object"][expected_section].is_object());
        }
    }

    #[test]
    fn large_projection_lists_are_paged_from_raw_bytes() {
        let mft_bytes = std::fs::read(
            "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
        )
        .expect("mft");
        let (total, page) = manifest_file_entries_page(&mft_bytes, 1, 3).expect("mft page");
        assert!(total >= 3);
        assert_eq!(page.len(), 3);
        assert!(page[0]["fileName"].as_str().is_some());
        let (_, empty_page) =
            manifest_file_entries_page(&mft_bytes, total + 10, 3).expect("empty page");
        assert!(empty_page.is_empty());

        let crl_bytes = std::fs::read(
            "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
        )
        .expect("crl");
        let (total, page) = crl_revoked_entries_page(&crl_bytes, 0, 5).expect("crl page");
        assert!(page.len() <= total);
        let (_, empty_page) =
            crl_revoked_entries_page(&crl_bytes, total + 10, 5).expect("empty crl page");
        assert!(empty_page.is_empty());
    }
}
