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

pub use decode::{CirDecodeError, decode_cir};
pub use encode::{CirEncodeError, encode_cir};
pub use materialize::{
    CirMaterializeError, CirMaterializeSummary, materialize_cir, mirror_relative_path_for_rsync_uri,
    resolve_static_pool_file,
};
pub use model::{
    CIR_VERSION_V1, CirHashAlgorithm, CirObject, CirTal, CanonicalInputRepresentation,
};
#[cfg(feature = "full")]
pub use export::{CirExportError, CirExportSummary, build_cir_from_run, export_cir_from_run, write_cir_file};
#[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_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
        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_cir() -> CanonicalInputRepresentation {
        CanonicalInputRepresentation {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: 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],
                },
            ],
            tals: vec![CirTal {
                tal_uri: "https://tal.example.net/root.tal".to_string(),
                tal_bytes:
                    b"https://tal.example.net/ta.cer\nrsync://example.net/repo/ta.cer\nMIIB"
                        .to_vec(),
            }],
        }
    }

    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 {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: Vec::new(),
            tals: vec![CirTal {
                tal_uri: "https://tal.example.net/minimal.tal".to_string(),
                tal_bytes: b"rsync://example.net/repo/ta.cer\nMIIB".to_vec(),
            }],
        };
        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 {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: 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],
                },
            ],
            tals: vec![CirTal {
                tal_uri: "https://tal.example.net/root.tal".to_string(),
                tal_bytes: b"x".to_vec(),
            }],
        };
        let err = encode_cir(&cir).expect_err("unsorted objects must fail");
        assert!(err.to_string().contains("CIR.objects"), "{err}");
    }

    #[test]
    fn cir_model_rejects_duplicate_tals() {
        let cir = CanonicalInputRepresentation {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: Vec::new(),
            tals: vec![
                CirTal {
                    tal_uri: "https://tal.example.net/root.tal".to_string(),
                    tal_bytes: b"a".to_vec(),
                },
                CirTal {
                    tal_uri: "https://tal.example.net/root.tal".to_string(),
                    tal_bytes: b"b".to_vec(),
                },
            ],
        };
        let err = encode_cir(&cir).expect_err("duplicate tals must fail");
        assert!(err.to_string().contains("CIR.tals"), "{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_V1 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 {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: vec![CirObject {
                rsync_uri: "https://example.net/repo/a.roa".to_string(),
                sha256: vec![0x11; 32],
            }],
            tals: vec![CirTal {
                tal_uri: "https://tal.example.net/root.tal".to_string(),
                tal_bytes: b"x".to_vec(),
            }],
        };
        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 {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: Vec::new(),
            tals: Vec::new(),
        };
        let err = encode_cir(&no_tals).expect_err("empty tals must fail");
        assert!(err.to_string().contains("CIR.tals 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 {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()),
            objects: Vec::new(),
            tals: vec![CirTal {
                tal_uri: "https://tal.example.net/root.tal".to_string(),
                tal_bytes: b"x".to_vec(),
            }],
        };
        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 {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: vec![CirObject {
                rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
                sha256: vec![0x11; 31],
            }],
            tals: vec![CirTal {
                tal_uri: "https://tal.example.net/root.tal".to_string(),
                tal_bytes: b"x".to_vec(),
            }],
        };
        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 {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: Vec::new(),
            tals: vec![CirTal {
                tal_uri: "ftp://tal.example.net/root.tal".to_string(),
                tal_bytes: b"x".to_vec(),
            }],
        };
        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 tal = test_encode_tlv(
            0x30,
            &[
                test_encode_tlv(0x16, b"https://tal.example.net/root.tal"),
                test_encode_tlv(0x04, b"x"),
            ]
            .concat(),
        );
        let bad = test_encode_tlv(
            0x30,
            &[
                test_encode_tlv(0x02, &[CIR_VERSION_V1 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, &tal),
            ]
            .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}");
    }
}
