use rpki::data_model::common::BigUnsigned;
use rpki::data_model::manifest::{
    ManifestDecodeError, ManifestEContent, ManifestObject, ManifestParseError, ManifestProfileError,
};
use rpki::data_model::signed_object::RpkiSignedObject;

fn len_bytes(len: usize) -> Vec<u8> {
    if len < 128 {
        vec![len as u8]
    } else {
        let mut tmp = Vec::new();
        let mut n = len;
        while n > 0 {
            tmp.push((n & 0xFF) as u8);
            n >>= 8;
        }
        tmp.reverse();
        let mut out = vec![0x80 | (tmp.len() as u8)];
        out.extend(tmp);
        out
    }
}

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

fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
    tlv(0x02, bytes)
}

fn der_integer_u64(v: u64) -> Vec<u8> {
    let mut bytes = Vec::new();
    let mut n = v;
    if n == 0 {
        bytes.push(0);
    } else {
        while n > 0 {
            bytes.push((n & 0xFF) as u8);
            n >>= 8;
        }
        bytes.reverse();
        if bytes[0] & 0x80 != 0 {
            bytes.insert(0, 0);
        }
    }
    der_integer_bytes(&bytes)
}

fn der_oid(oid: &str) -> Vec<u8> {
    use der_parser::asn1_rs::ToDer;
    use std::str::FromStr;
    let oid = der_parser::Oid::from_str(oid).unwrap();
    oid.to_der_vec().unwrap()
}

fn der_generalized_time_z(s: &str) -> Vec<u8> {
    tlv(0x18, s.as_bytes())
}

fn der_ia5(s: &str) -> Vec<u8> {
    tlv(0x16, s.as_bytes())
}

fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
    let mut content = vec![unused];
    content.extend_from_slice(bytes);
    tlv(0x03, &content)
}

fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
    let mut content = Vec::new();
    for c in children {
        content.extend(c);
    }
    tlv(0x30, &content)
}

fn der_cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
    tlv(0xA0 | (tag_no & 0x1F), &inner_der)
}

fn manifest_der(
    version: Option<u64>,
    manifest_number: Vec<u8>,
    this_update: Vec<u8>,
    next_update: Vec<u8>,
    file_hash_alg_oid: &str,
    file_list: Vec<Vec<u8>>,
) -> Vec<u8> {
    let mut fields = Vec::new();
    if let Some(v) = version {
        fields.push(der_cs_explicit(0, der_integer_u64(v)));
    }
    fields.push(der_integer_bytes(&manifest_number));
    fields.push(this_update);
    fields.push(next_update);
    fields.push(der_oid(file_hash_alg_oid));
    fields.push(der_sequence(file_list));
    der_sequence(fields)
}

fn file_and_hash(file: &str, unused: u8, hash: &[u8]) -> Vec<u8> {
    der_sequence(vec![der_ia5(file), der_bit_string(unused, hash)])
}

#[test]
fn manifest_econtent_version_must_be_zero_when_present() {
    let der = manifest_der(
        Some(1),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestVersion(1))
    ));
}

#[test]
fn manifest_number_too_long_rejected() {
    let long = vec![1u8; 21];
    let der = manifest_der(
        Some(0),
        long,
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::ManifestNumberTooLong)
    ));
}

#[test]
fn manifest_number_negative_rejected() {
    // INTEGER -1 encoded as 0xFF
    let der = manifest_der(
        Some(0),
        vec![0xFF],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestNumber)
    ));
}

#[test]
fn this_update_and_next_update_must_be_generalized_time() {
    let der = manifest_der(
        Some(0),
        vec![1],
        tlv(0x17, b"240101000000Z"), // UTCTime
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidThisUpdate)
    ));

    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        tlv(0x17, b"240101000000Z"), // UTCTime
        rpki::data_model::oid::OID_SHA256,
        vec![],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidNextUpdate)
    ));
}

#[test]
fn next_update_must_be_later() {
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260102000000Z"),
        der_generalized_time_z("20260101000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::NextUpdateNotLater)
    ));
}

#[test]
fn file_hash_alg_must_be_sha256() {
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        "1.2.3.4",
        vec![],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidFileHashAlg(_))
    ));
}

#[test]
fn file_list_entry_validation() {
    let hash = vec![0u8; 32];
    // Invalid filename (bad char)
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![file_and_hash("bad!.roa", 0, &hash)],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidFileName(_))
    ));

    // Non-octet-aligned BIT STRING
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![file_and_hash("ok.roa", 1, &hash)],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::HashNotOctetAligned)
    ));

    // Wrong hash length
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![file_and_hash("ok.roa", 0, &[0u8; 31])],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidHashLength(31))
    ));
}

#[test]
fn manifest_object_requires_correct_econtent_type() {
    let so_der = std::fs::read(
        "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
    )
    .expect("read MFT fixture");
    let mut so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object");
    so.signed_data.encap_content_info.econtent_type = "1.2.3.4".to_string();
    let err = ManifestObject::from_signed_object(so).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidEContentType(_))
    ));
}

#[test]
fn manifest_sequence_length_is_validated() {
    let der = der_sequence(vec![der_integer_u64(0)]);
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestSequenceLen(_))
    ));
}

#[test]
fn file_list_must_be_sequence_and_entry_shape_validated() {
    // Patch last byte to change SEQUENCE tag 0x30 to NULL tag 0x05 in the inner fileList.
    // Build a manifest with fileList = NULL explicitly.
    let der = {
        let mut fields = Vec::new();
        fields.push(der_cs_explicit(0, der_integer_u64(0)));
        fields.push(der_integer_u64(1));
        fields.push(der_generalized_time_z("20260101000000Z"));
        fields.push(der_generalized_time_z("20260102000000Z"));
        fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
        fields.push(tlv(0x05, &[]));
        der_sequence(fields)
    };
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidFileList)
    ));

    // FileAndHash not SEQUENCE of 2
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![der_sequence(vec![der_ia5("ok.roa")])],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidFileAndHash)
    ));
}

#[test]
fn version_tag_must_be_context_specific_0() {
    let der = {
        let mut fields = Vec::new();
        // Wrong tag number [1]
        fields.push(der_cs_explicit(1, der_integer_u64(0)));
        fields.push(der_integer_u64(1));
        fields.push(der_generalized_time_z("20260101000000Z"));
        fields.push(der_generalized_time_z("20260102000000Z"));
        fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
        fields.push(der_sequence(vec![]));
        der_sequence(fields)
    };
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::ProfileDecode(_))
    ));
}

#[test]
fn parse_manifest_number_helper_exercises_zero_encoding() {
    let z = der_parser::num_bigint::BigUint::from(0u32);
    let n = BigUnsigned::from_biguint(&z);
    assert_eq!(n.bytes_be, vec![0]);
}

#[test]
fn manifest_econtent_trailing_bytes_are_rejected() {
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![],
    );
    let mut bad = der.clone();
    bad.push(0);
    let err = ManifestEContent::decode_der(&bad).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Parse(ManifestParseError::TrailingBytes(1))
    ));
}

#[test]
fn manifest_version_rejects_trailing_bytes_inside_explicit_tag() {
    let mut version_inner = der_integer_u64(0);
    version_inner.extend(tlv(0x05, &[]));

    let der = {
        let mut fields = Vec::new();
        fields.push(der_cs_explicit(0, version_inner));
        fields.push(der_integer_u64(1));
        fields.push(der_generalized_time_z("20260101000000Z"));
        fields.push(der_generalized_time_z("20260102000000Z"));
        fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
        fields.push(der_sequence(vec![]));
        der_sequence(fields)
    };
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::ProfileDecode(_))
    ));
}

#[test]
fn manifest_rejects_hash_with_wrong_type() {
    let hash = vec![0u8; 32];
    let entry = der_sequence(vec![der_ia5("ok.roa"), tlv(0x04, &hash)]); // OCTET STRING, not BIT STRING
    let der = manifest_der(
        Some(0),
        vec![1],
        der_generalized_time_z("20260101000000Z"),
        der_generalized_time_z("20260102000000Z"),
        rpki::data_model::oid::OID_SHA256,
        vec![entry],
    );
    let err = ManifestEContent::decode_der(&der).unwrap_err();
    assert!(matches!(
        err,
        ManifestDecodeError::Validate(ManifestProfileError::InvalidHashType)
    ));
}

#[test]
fn file_name_validation_branches_are_exercised() {
    let hash = vec![0u8; 32];
    let cases = [
        "noext",  // missing '.'
        ".roa",   // empty base
        "a.roaa", // ext len != 3
        "a.r0a",  // ext not alphabetic
        "a.txt",  // ext not allowlisted
    ];
    for name in cases {
        let der = manifest_der(
            Some(0),
            vec![1],
            der_generalized_time_z("20260101000000Z"),
            der_generalized_time_z("20260102000000Z"),
            rpki::data_model::oid::OID_SHA256,
            vec![file_and_hash(name, 0, &hash)],
        );
        let err = ManifestEContent::decode_der(&der).unwrap_err();
        assert!(matches!(
            err,
            ManifestDecodeError::Validate(ManifestProfileError::InvalidFileName(_))
        ));
    }
}

#[test]
fn iana_registry_filename_extensions_are_accepted() {
    let hash = vec![0u8; 32];
    for ext in rpki::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS {
        let name = format!("ok.{ext}");
        let der = manifest_der(
            Some(0),
            vec![1],
            der_generalized_time_z("20260101000000Z"),
            der_generalized_time_z("20260102000000Z"),
            rpki::data_model::oid::OID_SHA256,
            vec![file_and_hash(&name, 0, &hash)],
        );
        ManifestEContent::decode_der(&der).expect("manifest should accept IANA extension");
    }
}
