//! Property-based tests for `BodyStructure` (RFC 3501 Section 7.4.2).
//!
//! Uses `proptest` with `prop_recursive` to generate arbitrarily nested MIME
//! tree structures and verify structural invariants.
//!
//! # References
//! - RFC 3501 Section 7.4.2 (BODYSTRUCTURE)
//! - RFC 9051 Section 7.5.2 (BODYSTRUCTURE, `IMAP4rev2`)
//! - RFC 2045 Section 5.1 (Content-Type)
//! - RFC 2046 Section 5 (Multipart)
//! - RFC 2183 (Content-Disposition)

use daaki_imap::types::{BodyStructure, ContentDisposition, Envelope, EnvelopeAddress};
use proptest::prelude::*;

// ---------------------------------------------------------------------------
// Leaf generators
// ---------------------------------------------------------------------------

/// Generates an `EnvelopeAddress` (RFC 3501 Section 7.4.2).
fn arb_envelope_address() -> impl Strategy<Value = EnvelopeAddress> {
    (
        proptest::option::of("[A-Za-z ]{1,20}"),
        proptest::option::of("[a-z]{1,10}"),
        proptest::option::of("[a-z]{1,10}"),
        proptest::option::of("[a-z]{1,10}\\.[a-z]{2,4}"),
    )
        .prop_map(|(name, adl, mailbox, host)| {
            let mut s = EnvelopeAddress::default();
            s.name = name;
            s.adl = adl;
            s.mailbox = mailbox;
            s.host = host;
            s
        })
}

/// Generates an `Envelope` (RFC 3501 Section 7.4.2).
fn arb_envelope() -> impl Strategy<Value = Envelope> {
    (
        proptest::option::of("[A-Za-z0-9, :]{5,30}"),
        proptest::option::of("[A-Za-z0-9 ]{1,40}"),
        proptest::collection::vec(arb_envelope_address(), 0..3),
        proptest::collection::vec(arb_envelope_address(), 0..3),
        proptest::collection::vec(arb_envelope_address(), 0..3),
        proptest::collection::vec(arb_envelope_address(), 0..3),
        proptest::collection::vec(arb_envelope_address(), 0..2),
        proptest::collection::vec(arb_envelope_address(), 0..2),
        proptest::option::of("<[a-z0-9]{4,12}@[a-z]{3,8}\\.[a-z]{2,4}>"),
        proptest::option::of("<[a-z0-9]{4,12}@[a-z]{3,8}\\.[a-z]{2,4}>"),
    )
        .prop_map(
            |(date, subject, from, sender, reply_to, to, cc, bcc, in_reply_to, message_id)| {
                let mut s = Envelope::default();
                s.date = date;
                s.subject = subject;
                s.from = from;
                s.sender = sender;
                s.reply_to = reply_to;
                s.to = to;
                s.cc = cc;
                s.bcc = bcc;
                s.in_reply_to = in_reply_to;
                s.message_id = message_id;
                s
            },
        )
}

/// Generates a `ContentDisposition` (RFC 2183 Section 2).
fn arb_content_disposition() -> impl Strategy<Value = ContentDisposition> {
    (
        prop_oneof![Just("inline".to_owned()), Just("attachment".to_owned()),],
        proptest::collection::vec(
            (
                prop_oneof![
                    Just("filename".to_owned()),
                    Just("size".to_owned()),
                    Just("creation-date".to_owned()),
                ],
                "[a-zA-Z0-9._-]{1,20}",
            ),
            0..3,
        ),
    )
        .prop_map(|(disposition_type, params)| {
            let mut s = ContentDisposition::default();
            s.disposition_type = disposition_type;
            s.params = params;
            s
        })
}

/// Generates optional extension fields common to single-part bodies
/// (RFC 3501 Section 7.4.2 extension data).
fn arb_extension_fields() -> impl Strategy<
    Value = (
        Option<String>,
        Option<ContentDisposition>,
        Option<Vec<String>>,
        Option<String>,
    ),
> {
    (
        proptest::option::of("[a-f0-9]{32}"),
        proptest::option::of(arb_content_disposition()),
        proptest::option::of(proptest::collection::vec(
            prop_oneof![
                Just("en".to_owned()),
                Just("de".to_owned()),
                Just("fr".to_owned()),
                Just("ja".to_owned()),
            ],
            1..4,
        )),
        proptest::option::of("https?://[a-z]{3,10}\\.[a-z]{2,4}/[a-z]{1,10}"),
    )
}

/// MIME content-type parameters (RFC 2045 Section 5.1).
fn arb_params() -> impl Strategy<Value = Vec<(String, String)>> {
    proptest::collection::vec(
        (
            prop_oneof![
                Just("charset".to_owned()),
                Just("name".to_owned()),
                Just("boundary".to_owned()),
            ],
            "[a-zA-Z0-9_-]{1,20}",
        ),
        0..3,
    )
}

/// Content-Transfer-Encoding values (RFC 2045 Section 6).
fn arb_encoding() -> impl Strategy<Value = String> {
    prop_oneof![
        Just("7bit".to_owned()),
        Just("8bit".to_owned()),
        Just("binary".to_owned()),
        Just("quoted-printable".to_owned()),
        Just("base64".to_owned()),
    ]
}

/// Optional Content-ID (RFC 2045 Section 7).
fn arb_content_id() -> impl Strategy<Value = Option<String>> {
    proptest::option::of("<[a-z0-9]{4,12}@[a-z]{3,8}\\.[a-z]{2,4}>")
}

/// Optional Content-Description (RFC 2045 Section 8).
fn arb_description() -> impl Strategy<Value = Option<String>> {
    proptest::option::of("[A-Za-z0-9 ]{1,30}")
}

/// Generates a `BodyStructure::Basic` leaf (RFC 3501 Section 7.4.2).
fn arb_basic_body() -> impl Strategy<Value = BodyStructure> {
    (
        prop_oneof![
            Just("image".to_owned()),
            Just("application".to_owned()),
            Just("audio".to_owned()),
            Just("video".to_owned()),
        ],
        prop_oneof![
            Just("png".to_owned()),
            Just("pdf".to_owned()),
            Just("octet-stream".to_owned()),
            Just("jpeg".to_owned()),
            Just("mp3".to_owned()),
        ],
        arb_params(),
        arb_content_id(),
        arb_description(),
        arb_encoding(),
        0..1_000_000u64,
        arb_extension_fields(),
    )
        .prop_map(
            |(media_type, media_subtype, params, id, description, encoding, size, ext)| {
                BodyStructure::Basic {
                    media_type,
                    media_subtype,
                    params,
                    id,
                    description,
                    encoding,
                    size,
                    md5: ext.0,
                    disposition: ext.1,
                    language: ext.2,
                    location: ext.3,
                }
            },
        )
}

/// Generates a `BodyStructure::Text` leaf (RFC 3501 Section 7.4.2).
fn arb_text_body() -> impl Strategy<Value = BodyStructure> {
    (
        prop_oneof![
            Just("plain".to_owned()),
            Just("html".to_owned()),
            Just("enriched".to_owned()),
        ],
        arb_params(),
        arb_content_id(),
        arb_description(),
        arb_encoding(),
        0..1_000_000u64,
        0..100_000u64,
        arb_extension_fields(),
    )
        .prop_map(
            |(media_subtype, params, id, description, encoding, size, lines, ext)| {
                BodyStructure::Text {
                    media_subtype,
                    params,
                    id,
                    description,
                    encoding,
                    size,
                    lines,
                    md5: ext.0,
                    disposition: ext.1,
                    language: ext.2,
                    location: ext.3,
                }
            },
        )
}

/// Generates a `BodyStructure::Message` node (RFC 3501 Section 7.4.2).
///
/// Requires a strategy for the inner `BodyStructure` to support recursion.
fn arb_message_body(
    inner: impl Strategy<Value = BodyStructure> + 'static,
) -> impl Strategy<Value = BodyStructure> {
    (
        prop_oneof![Just("rfc822".to_owned()), Just("global".to_owned()),],
        arb_params(),
        arb_content_id(),
        arb_description(),
        arb_encoding(),
        0..1_000_000u64,
        arb_envelope(),
        inner,
        0..100_000u64,
        arb_extension_fields(),
    )
        .prop_map(
            |(
                media_subtype,
                params,
                id,
                description,
                encoding,
                size,
                envelope,
                body,
                lines,
                ext,
            )| {
                BodyStructure::Message {
                    media_subtype,
                    params,
                    id,
                    description,
                    encoding,
                    size,
                    envelope: Box::new(envelope),
                    body: Box::new(body),
                    lines,
                    md5: ext.0,
                    disposition: ext.1,
                    language: ext.2,
                    location: ext.3,
                }
            },
        )
}

/// Generates a `BodyStructure::Multipart` node (RFC 2046 Section 5).
///
/// Requires a strategy for child bodies to support recursion.
/// Multipart always has at least 1 child per RFC 2046 Section 5.1.1.
fn arb_multipart_body(
    inner: impl Strategy<Value = BodyStructure> + 'static,
) -> impl Strategy<Value = BodyStructure> {
    (
        prop_oneof![
            Just("mixed".to_owned()),
            Just("alternative".to_owned()),
            Just("related".to_owned()),
            Just("digest".to_owned()),
        ],
        proptest::collection::vec(inner, 1..5),
        arb_params(),
        proptest::option::of(arb_content_disposition()),
        proptest::option::of(proptest::collection::vec(
            prop_oneof![
                Just("en".to_owned()),
                Just("de".to_owned()),
                Just("fr".to_owned()),
            ],
            1..3,
        )),
        proptest::option::of("https?://[a-z]{3,10}\\.[a-z]{2,4}/[a-z]{1,10}"),
    )
        .prop_map(
            |(media_subtype, bodies, params, disposition, language, location)| {
                BodyStructure::Multipart {
                    media_subtype,
                    bodies,
                    params,
                    disposition,
                    language,
                    location,
                }
            },
        )
}

/// Combined recursive generator for `BodyStructure`
/// (RFC 3501 Section 7.4.2 / RFC 9051 Section 7.5.2).
///
/// Uses `prop_recursive` to generate trees bounded at:
/// - depth 3 (max nesting)
/// - 64 total nodes
/// - 10 items per collection
fn arb_body_structure() -> impl Strategy<Value = BodyStructure> {
    let leaf = prop_oneof![arb_basic_body(), arb_text_body(),];

    leaf.prop_recursive(
        3,  // max depth
        64, // max nodes
        10, // items per collection
        |inner| {
            prop_oneof![
                // Multipart contains children (RFC 2046 Section 5).
                arb_multipart_body(inner.clone()),
                // Message contains nested BodyStructure (RFC 3501 Section 7.4.2).
                arb_message_body(inner),
            ]
        },
    )
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Compute the depth of a `BodyStructure` tree.
fn body_structure_depth(bs: &BodyStructure) -> usize {
    match bs {
        BodyStructure::Message { body, .. } => 1 + body_structure_depth(body),
        BodyStructure::Multipart { bodies, .. } => {
            1 + bodies.iter().map(body_structure_depth).max().unwrap_or(0)
        }
        _ => 1,
    }
}

/// Count total nodes in a `BodyStructure` tree.
fn body_structure_node_count(bs: &BodyStructure) -> usize {
    match bs {
        BodyStructure::Message { body, .. } => 1 + body_structure_node_count(body),
        BodyStructure::Multipart { bodies, .. } => {
            1 + bodies.iter().map(body_structure_node_count).sum::<usize>()
        }
        _ => 1,
    }
}

/// Recursively check structural invariants on a `BodyStructure`.
fn check_well_formed(bs: &BodyStructure) -> Result<(), String> {
    match bs {
        BodyStructure::Basic {
            media_type,
            media_subtype,
            encoding,
            ..
        } => {
            // RFC 2045 Section 5.1: media-type and subtype must be non-empty.
            if media_type.is_empty() {
                return Err("Basic: media_type is empty".into());
            }
            if media_subtype.is_empty() {
                return Err("Basic: media_subtype is empty".into());
            }
            // RFC 2045 Section 6: encoding must be non-empty.
            if encoding.is_empty() {
                return Err("Basic: encoding is empty".into());
            }
            Ok(())
        }
        BodyStructure::Text {
            media_subtype,
            encoding,
            ..
        } => {
            // RFC 2045 Section 5.1: subtype must be non-empty.
            if media_subtype.is_empty() {
                return Err("Text: media_subtype is empty".into());
            }
            // RFC 2045 Section 6: encoding must be non-empty.
            if encoding.is_empty() {
                return Err("Text: encoding is empty".into());
            }
            Ok(())
        }
        BodyStructure::Message {
            media_subtype,
            encoding,
            envelope,
            body,
            ..
        } => {
            // RFC 9051 Section 7.5.2: subtype is "rfc822" or "global".
            if media_subtype.is_empty() {
                return Err("Message: media_subtype is empty".into());
            }
            if encoding.is_empty() {
                return Err("Message: encoding is empty".into());
            }
            // RFC 3501 Section 7.4.2: envelope must be present (we use Box, so always is).
            // Verify envelope is accessible.
            let _ = &envelope.date;
            // Recurse into nested body.
            check_well_formed(body)?;
            Ok(())
        }
        BodyStructure::Multipart {
            media_subtype,
            bodies,
            ..
        } => {
            // RFC 2046 Section 5.1.1: multipart must have a subtype.
            if media_subtype.is_empty() {
                return Err("Multipart: media_subtype is empty".into());
            }
            // RFC 2046 Section 5.1.1: "one or more body parts" — at least one child.
            if bodies.is_empty() {
                return Err("Multipart: bodies is empty (RFC 2046 Section 5.1.1)".into());
            }
            // Recurse into each child.
            for (i, child) in bodies.iter().enumerate() {
                check_well_formed(child).map_err(|e| format!("Multipart child {i}: {e}"))?;
            }
            Ok(())
        }
        _ => Ok(()),
    }
}

// ---------------------------------------------------------------------------
// Property tests
// ---------------------------------------------------------------------------

proptest! {
    #![proptest_config(ProptestConfig::with_cases(256))]

    /// Every generated `BodyStructure` satisfies structural invariants
    /// (RFC 3501 Section 7.4.2, RFC 2045 Section 5.1, RFC 2046 Section 5.1.1).
    ///
    /// - Basic/Text/Message: non-empty media type/subtype and encoding
    /// - Multipart: at least one child body part
    /// - Message: envelope and nested body are present
    /// - All nodes recursively satisfy the same invariants
    #[test]
    fn body_structure_is_well_formed(bs in arb_body_structure()) {
        if let Err(e) = check_well_formed(&bs) {
            return Err(proptest::test_runner::TestCaseError::Fail(
                format!("well-formedness check failed: {e}").into(),
            ));
        }
    }

    /// Recursion depth never exceeds the configured maximum.
    ///
    /// `prop_recursive` is configured with max depth 3. Leaf nodes have depth 1,
    /// so the overall tree depth should not exceed 4 (1 base + 3 recursive levels).
    /// We use a generous bound of 8 to account for proptest internal behavior.
    #[test]
    fn body_structure_depth_bounded(bs in arb_body_structure()) {
        let depth = body_structure_depth(&bs);
        prop_assert!(
            depth <= 8,
            "depth {} exceeds bound 8",
            depth
        );
    }

    /// Total node count stays within the configured maximum.
    ///
    /// `prop_recursive` is configured with max 64 nodes. We allow some headroom
    /// since the bound is approximate.
    #[test]
    fn body_structure_node_count_bounded(bs in arb_body_structure()) {
        let count = body_structure_node_count(&bs);
        prop_assert!(
            count <= 256,
            "node count {} exceeds bound 256",
            count
        );
    }

    /// Clone produces an identical tree (sanity check on derived Clone + Eq).
    #[test]
    fn body_structure_clone_equals_original(bs in arb_body_structure()) {
        let cloned = bs.clone();
        prop_assert_eq!(&bs, &cloned);
    }
}
