pub mod accumulator;
pub mod decode;
pub mod encode;
#[cfg(feature = "full")]
pub mod export;
#[cfg(feature = "full")]
pub mod materialize;
pub mod model;
pub mod sequence;
#[cfg(feature = "full")]
pub mod static_pool;

pub use accumulator::{CirInputAccumulator, CirInputSection, CirInputSnapshot};
pub use decode::{CirDecodeError, decode_cir};
pub use encode::{CirEncodeError, encode_cir};
#[cfg(feature = "full")]
pub use export::{
    CirExportError, CirExportSummary, CirTrustAnchorBinding, build_cir_from_input_snapshot_multi,
    build_cir_from_run, build_cir_from_run_multi, export_cir_from_input_snapshot_multi,
    export_cir_from_run, export_cir_from_run_multi, write_cir_file,
};
#[cfg(feature = "full")]
pub use materialize::{
    CirMaterializeError, CirMaterializeSummary, materialize_cir, materialize_cir_from_raw_store,
    materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file,
};
pub use model::{
    CIR_VERSION_V1, CIR_VERSION_V3, CIR_VERSION_V4, CanonicalInputRepresentation, CirHashAlgorithm,
    CirObject, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256,
};
pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind};
#[cfg(feature = "full")]
pub use static_pool::{
    CirStaticPoolError, CirStaticPoolExportSummary, CirStaticPoolWriteResult,
    export_hashes_from_store, static_pool_path, static_pool_relative_path,
    write_bytes_to_static_pool, write_raw_entry_to_static_pool,
};

#[cfg(test)]
mod tests {
    use super::{
        CIR_VERSION_V4, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor,
        decode_cir, encode_cir,
    };

    fn sample_time() -> time::OffsetDateTime {
        time::OffsetDateTime::parse(
            "2026-04-07T12:34:56Z",
            &time::format_description::well_known::Rfc3339,
        )
        .expect("valid rfc3339")
    }

    fn sample_trust_anchor(ta_rsync_uri: &str, tal_uri: &str, ta_der: &[u8]) -> CirTrustAnchor {
        CirTrustAnchor {
            ta_rsync_uri: ta_rsync_uri.to_string(),
            tal_uri: tal_uri.to_string(),
            tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
            ta_certificate_der: ta_der.to_vec(),
            ta_certificate_sha256: super::sha256(ta_der),
        }
    }

    fn sample_cir() -> CanonicalInputRepresentation {
        let rejected_objects = vec![
            CirRejectedObject {
                object_uri: "rsync://example.net/repo/rejected-a.roa".to_string(),
                reason: Some("invalid roa".to_string()),
            },
            CirRejectedObject {
                object_uri: "rsync://example.net/repo/rejected-b.asa".to_string(),
                reason: None,
            },
        ];
        CanonicalInputRepresentation::new_v4(
            sample_time(),
            vec![
                CirObject {
                    rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
                    sha256: vec![0x11; 32],
                },
                CirObject {
                    rsync_uri: "rsync://example.net/repo/b.roa".to_string(),
                    sha256: vec![0x22; 32],
                },
                CirObject {
                    rsync_uri: "rsync://example.net/repo/rejected-a.roa".to_string(),
                    sha256: vec![0x33; 32],
                },
                CirObject {
                    rsync_uri: "rsync://example.net/repo/rejected-b.asa".to_string(),
                    sha256: vec![0x44; 32],
                },
            ],
            Vec::new(),
            vec![sample_trust_anchor(
                "rsync://example.net/repo/ta.cer",
                "https://tal.example.net/root.tal",
                b"ta-der",
            )],
            rejected_objects,
            Vec::new(),
        )
    }

    fn test_encode_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
        let mut out = Vec::with_capacity(8 + value.len());
        out.push(tag);
        if value.len() < 0x80 {
            out.push(value.len() as u8);
        } else {
            let len = value.len();
            let bytes = len.to_be_bytes();
            let first_non_zero = bytes
                .iter()
                .position(|&b| b != 0)
                .unwrap_or(bytes.len() - 1);
            let len_bytes = &bytes[first_non_zero..];
            out.push(0x80 | len_bytes.len() as u8);
            out.extend_from_slice(len_bytes);
        }
        out.extend_from_slice(value);
        out
    }

    #[test]
    fn cir_roundtrip_full_succeeds() {
        let cir = sample_cir();
        let der = encode_cir(&cir).expect("encode cir");
        let decoded = decode_cir(&der).expect("decode cir");
        assert_eq!(decoded, cir);
    }

    #[test]
    fn cir_roundtrip_minimal_succeeds() {
        let cir = CanonicalInputRepresentation::new_v4(
            sample_time(),
            Vec::new(),
            Vec::new(),
            vec![sample_trust_anchor(
                "rsync://example.net/repo/minimal-ta.cer",
                "https://tal.example.net/minimal.tal",
                b"minimal-ta-der",
            )],
            Vec::new(),
            Vec::new(),
        );
        let der = encode_cir(&cir).expect("encode minimal cir");
        let decoded = decode_cir(&der).expect("decode minimal cir");
        assert_eq!(decoded, cir);
    }

    #[test]
    fn cir_model_rejects_unsorted_duplicate_objects() {
        let cir = CanonicalInputRepresentation::new_v4(
            sample_time(),
            vec![
                CirObject {
                    rsync_uri: "rsync://example.net/repo/z.roa".to_string(),
                    sha256: vec![0x11; 32],
                },
                CirObject {
                    rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
                    sha256: vec![0x22; 32],
                },
            ],
            Vec::new(),
            vec![sample_trust_anchor(
                "rsync://example.net/repo/ta.cer",
                "https://tal.example.net/root.tal",
                b"ta-der",
            )],
            Vec::new(),
            Vec::new(),
        );
        let err = encode_cir(&cir).expect_err("unsorted objects must fail");
        assert!(
            err.to_string().contains("CIR.freshValidatedObjects"),
            "{err}"
        );
    }

    #[test]
    fn cir_model_rejects_duplicate_tals() {
        let cir = CanonicalInputRepresentation::new_v4(
            sample_time(),
            Vec::new(),
            Vec::new(),
            vec![
                sample_trust_anchor(
                    "rsync://example.net/repo/ta.cer",
                    "https://tal.example.net/root.tal",
                    b"ta-der-a",
                ),
                sample_trust_anchor(
                    "rsync://example.net/repo/ta.cer",
                    "https://tal.example.net/root.tal",
                    b"ta-der-b",
                ),
            ],
            Vec::new(),
            Vec::new(),
        );
        let err = encode_cir(&cir).expect_err("duplicate trust_anchors must fail");
        assert!(err.to_string().contains("CIR.trustAnchors"), "{err}");
    }

    #[test]
    fn cir_decode_rejects_wrong_version() {
        let mut der = encode_cir(&sample_cir()).expect("encode cir");
        let pos = der
            .windows(3)
            .position(|window| window == [0x02, 0x01, CIR_VERSION_V4 as u8])
            .or_else(|| {
                der.windows(3)
                    .position(|window| window == [0x02, 0x01, CIR_VERSION_V4 as u8])
            })
            .expect("find version integer");
        der[pos + 2] = 2;
        let err = decode_cir(&der).expect_err("wrong version must fail");
        assert!(err.to_string().contains("unexpected CIR version"), "{err}");
    }

    #[test]
    fn cir_decode_rejects_wrong_hash_oid() {
        let mut der = encode_cir(&sample_cir()).expect("encode cir");
        let sha256_bytes = crate::data_model::oid::OID_SHA256_RAW;
        let idx = der
            .windows(sha256_bytes.len())
            .position(|window| window == sha256_bytes)
            .expect("find sha256 oid");
        der[idx + sha256_bytes.len() - 1] ^= 0x01;
        let err = decode_cir(&der).expect_err("wrong oid must fail");
        assert!(
            err.to_string().contains(crate::data_model::oid::OID_SHA256),
            "{err}"
        );
    }

    #[test]
    fn cir_decode_rejects_bad_generalized_time() {
        let mut der = encode_cir(&sample_cir()).expect("encode cir");
        let pos = der
            .windows(15)
            .position(|window| window == b"20260407123456Z")
            .expect("find generalized time");
        der[pos + 14] = b'X';
        let err = decode_cir(&der).expect_err("bad time must fail");
        assert!(err.to_string().contains("GeneralizedTime"), "{err}");
    }

    #[test]
    fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() {
        let bad_object = CanonicalInputRepresentation::new_v4(
            sample_time(),
            vec![CirObject {
                rsync_uri: "https://example.net/repo/a.roa".to_string(),
                sha256: vec![0x11; 32],
            }],
            Vec::new(),
            vec![sample_trust_anchor(
                "rsync://example.net/repo/ta.cer",
                "https://tal.example.net/root.tal",
                b"ta-der",
            )],
            Vec::new(),
            Vec::new(),
        );
        let err = encode_cir(&bad_object).expect_err("non-rsync object uri must fail");
        assert!(err.to_string().contains("rsync://"), "{err}");

        let no_tals = CanonicalInputRepresentation::new_v4(
            sample_time(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
        );
        let err = encode_cir(&no_tals).expect_err("empty trust_anchors must fail");
        assert!(
            err.to_string()
                .contains("CIR.trustAnchors must be non-empty"),
            "{err}"
        );
    }

    #[test]
    fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() {
        let bad_time = CanonicalInputRepresentation::new_v4(
            sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()),
            Vec::new(),
            Vec::new(),
            vec![sample_trust_anchor(
                "rsync://example.net/repo/ta.cer",
                "https://tal.example.net/root.tal",
                b"ta-der",
            )],
            Vec::new(),
            Vec::new(),
        );
        let bad_time = CanonicalInputRepresentation {
            validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()),
            ..bad_time
        };
        let err = encode_cir(&bad_time).expect_err("non-utc validation time must fail");
        assert!(err.to_string().contains("UTC"), "{err}");

        let bad_hash = CanonicalInputRepresentation::new_v4(
            sample_time(),
            vec![CirObject {
                rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
                sha256: vec![0x11; 31],
            }],
            Vec::new(),
            vec![sample_trust_anchor(
                "rsync://example.net/repo/ta.cer",
                "https://tal.example.net/root.tal",
                b"ta-der",
            )],
            Vec::new(),
            Vec::new(),
        );
        let err = encode_cir(&bad_hash).expect_err("bad digest len must fail");
        assert!(err.to_string().contains("32 bytes"), "{err}");

        let bad_tal_uri = CanonicalInputRepresentation::new_v4(
            sample_time(),
            Vec::new(),
            Vec::new(),
            vec![sample_trust_anchor(
                "rsync://example.net/repo/ta.cer",
                "ftp://tal.example.net/root.tal",
                b"ta-der",
            )],
            Vec::new(),
            Vec::new(),
        );
        let err = encode_cir(&bad_tal_uri).expect_err("bad tal uri must fail");
        assert!(err.to_string().contains("http:// or https://"), "{err}");
    }

    #[test]
    fn cir_decode_rejects_trailing_bytes_and_trailing_fields() {
        let cir = sample_cir();
        let mut der = encode_cir(&cir).expect("encode cir");
        der.push(0);
        let err = decode_cir(&der).expect_err("trailing bytes after cir must fail");
        assert!(
            err.to_string().contains("trailing bytes after CIR"),
            "{err}"
        );

        let object = test_encode_tlv(
            0x30,
            &[
                test_encode_tlv(0x16, b"rsync://example.net/repo/a.roa"),
                test_encode_tlv(0x04, &[0x11; 32]),
                test_encode_tlv(0x02, &[0x01]),
            ]
            .concat(),
        );
        let trust_anchor = test_encode_tlv(
            0x30,
            &[
                test_encode_tlv(0x16, b"rsync://example.net/repo/ta.cer"),
                test_encode_tlv(0x16, b"https://tal.example.net/root.tal"),
                test_encode_tlv(0x04, b"rsync://example.net/repo/ta.cer\n\nAQID\n"),
                test_encode_tlv(0x04, b"ta-der"),
                test_encode_tlv(0x04, &super::sha256(b"ta-der")),
            ]
            .concat(),
        );
        let bad = test_encode_tlv(
            0x30,
            &[
                test_encode_tlv(0x02, &[CIR_VERSION_V4 as u8]),
                test_encode_tlv(0x06, crate::data_model::oid::OID_SHA256_RAW),
                test_encode_tlv(0x18, b"20260407123456Z"),
                test_encode_tlv(0x30, &object),
                test_encode_tlv(0x30, &[]),
                test_encode_tlv(0x30, &trust_anchor),
                test_encode_tlv(0x04, &[0x33; 32]),
                test_encode_tlv(0x04, &[0x33; 32]),
                test_encode_tlv(0x04, &[0x33; 32]),
                test_encode_tlv(0x04, &[0x33; 32]),
                test_encode_tlv(0x04, &[0x33; 32]),
                test_encode_tlv(0x04, &[0x33; 32]),
                test_encode_tlv(0x30, &[]),
                test_encode_tlv(0x30, &[]),
            ]
            .concat(),
        );
        let err = decode_cir(&bad).expect_err("trailing field in object must fail");
        assert!(
            err.to_string().contains("trailing fields in CirObject"),
            "{err}"
        );
    }

    #[test]
    fn cir_decode_rejects_invalid_object_and_tal_shapes() {
        let cir = sample_cir();
        let mut der = encode_cir(&cir).expect("encode cir");

        let rsync_text = b"rsync://example.net/repo/a.cer";
        let idx = der
            .windows(rsync_text.len())
            .position(|window| window == rsync_text)
            .expect("find object uri");
        der[idx] = 0xFF;
        let err = decode_cir(&der).expect_err("invalid utf8 object uri must fail");
        assert!(err.to_string().contains("utf-8"), "{err}");

        let mut der = encode_cir(&cir).expect("encode cir");
        let tal_text = b"https://tal.example.net/root.tal";
        let idx = der
            .windows(tal_text.len())
            .position(|window| window == tal_text)
            .expect("find tal uri");
        der[idx] = 0xFF;
        let err = decode_cir(&der).expect_err("invalid utf8 tal uri must fail");
        assert!(err.to_string().contains("utf-8"), "{err}");
    }

    #[test]
    fn cir_model_rejects_unsorted_rejected_objects_and_bad_digest() {
        let mut cir = sample_cir();
        cir.fresh_rejected_objects.swap(0, 1);
        let err = encode_cir(&cir).expect_err("unsorted rejected objects must fail");
        assert!(
            err.to_string().contains("CIR.freshRejectedObjects"),
            "{err}"
        );

        let mut cir = sample_cir();
        cir.reject_list_sha256 = vec![0x55; 32];
        let err = encode_cir(&cir).expect_err("bad reject list digest must fail");
        assert!(err.to_string().contains("rejectListSha256"), "{err}");
    }
}
