use darling::{util::Flag, FromDeriveInput, FromMeta};
use proc_macro2::Ident;
use syn::parse_quote;

#[derive(FromMeta)]
struct Vis {
    public: Flag,
    private: Flag,
}

#[derive(FromDeriveInput)]
#[darling(attributes(sample))]
struct Example {
    ident: Ident,
    label: String,
    #[darling(flatten)]
    visibility: Vis,
}

#[test]
fn happy_path() {
    let di = Example::from_derive_input(&parse_quote! {
        #[sample(label = "Hello", public)]
        struct Demo {}
    });

    let parsed = di.unwrap();
    assert_eq!(parsed.ident, "Demo");
    assert_eq!(&parsed.label, "Hello");
    assert!(parsed.visibility.public.is_present());
    assert!(!parsed.visibility.private.is_present());
}

#[test]
fn unknown_field_errors() {
    let errors = Example::from_derive_input(&parse_quote! {
        #[sample(label = "Hello", republic)]
        struct Demo {}
    })
    .map(|_| "Should have failed")
    .unwrap_err();

    assert_eq!(errors.len(), 1);
}

/// This test demonstrates flatten being used recursively.
/// Fields are expected to be consumed by the outermost matching struct.
#[test]
fn recursive_flattening() {
    #[derive(FromMeta)]
    struct Nested2 {
        above: isize,
        below: isize,
        port: Option<isize>,
    }

    #[derive(FromMeta)]
    struct Nested1 {
        port: isize,
        starboard: isize,
        #[darling(flatten)]
        z_axis: Nested2,
    }

    #[derive(FromMeta)]
    struct Nested0 {
        fore: isize,
        aft: isize,
        #[darling(flatten)]
        cross_section: Nested1,
    }

    #[derive(FromDeriveInput)]
    #[darling(attributes(boat))]
    struct BoatPosition {
        #[darling(flatten)]
        pos: Nested0,
    }

    let parsed = BoatPosition::from_derive_input(&parse_quote! {
        #[boat(fore = 1, aft = 1, port = 10, starboard = 50, above = 20, below = -3)]
        struct Demo;
    })
    .unwrap();

    assert_eq!(parsed.pos.fore, 1);
    assert_eq!(parsed.pos.aft, 1);

    assert_eq!(parsed.pos.cross_section.port, 10);
    assert_eq!(parsed.pos.cross_section.starboard, 50);

    assert_eq!(parsed.pos.cross_section.z_axis.above, 20);
    assert_eq!(parsed.pos.cross_section.z_axis.below, -3);
    // This should be `None` because the `port` field in `Nested1` consumed
    // the field before the leftovers were passed to `Nested2::from_list`.
    assert_eq!(parsed.pos.cross_section.z_axis.port, None);
}

/// This test confirms that a collection - in this case a HashMap - can
/// be used with `flatten`.
#[test]
fn flattening_into_hashmap() {
    #[derive(FromDeriveInput)]
    #[darling(attributes(ca))]
    struct Catchall {
        hello: String,
        volume: usize,
        #[darling(flatten)]
        others: std::collections::HashMap<String, String>,
    }

    let parsed = Catchall::from_derive_input(&parse_quote! {
        #[ca(hello = "World", volume = 10, first_name = "Alice", second_name = "Bob")]
        struct Demo;
    })
    .unwrap();

    assert_eq!(parsed.hello, "World");
    assert_eq!(parsed.volume, 10);
    assert_eq!(parsed.others.len(), 2);
}

#[derive(FromMeta)]
#[allow(dead_code)]
struct Person {
    first: String,
    last: String,
    parent: Option<Box<Person>>,
}

#[derive(FromDeriveInput)]
#[darling(attributes(v))]
#[allow(dead_code)]
struct Outer {
    #[darling(flatten)]
    owner: Person,
    #[darling(default)]
    blast: bool,
}

/// This test makes sure that field names from parent structs are not inappropriately
/// offered as alternates for unknown field errors in child structs.
///
/// A naive implementation that tried to offer all the flattened fields for "did you mean"
/// could inspect all errors returned by the flattened field's `from_list` call and add the
/// parent's field names as alternates to all unknown field errors.
///
/// THIS WOULD BE INCORRECT. Those unknown field errors may have already come from
/// child fields within the flattened struct, where the parent's field names are not valid.
#[test]
fn do_not_suggest_invalid_alts() {
    let errors = Outer::from_derive_input(&parse_quote! {
        #[v(first = "Hello", last = "World", parent(first = "Hi", last = "Earth", blasts = "off"))]
        struct Demo;
    })
    .map(|_| "Should have failed")
    .unwrap_err()
    .to_string();

    assert!(
        !errors.contains("`blast`"),
        "Should not contain `blast`: {}",
        errors
    );
}

#[cfg(feature = "suggestions")]
#[track_caller]
fn assert_error_has_suggestion(error: &darling::Error, field: &str) {
    let error = error.to_string();
    let backtick_field = format!("`{}`", field);
    assert!(
        error.contains(&backtick_field),
        "Should contain {} as did-you-mean suggestion: {}",
        backtick_field,
        error
    );
}

#[test]
#[cfg(feature = "suggestions")]
fn suggest_valid_parent_alts() {
    let errors = Outer::from_derive_input(&parse_quote! {
        #[v(first = "Hello", bladt = false, last = "World", parent(first = "Hi", last = "Earth"))]
        struct Demo;
    })
    .map(|_| "Should have failed")
    .unwrap_err();
    assert_error_has_suggestion(&errors, "blast");
}

#[test]
#[cfg(feature = "suggestions")]
fn suggest_alts_for_multiple_errors() {
    let errors = Example::from_derive_input(&parse_quote! {
        #[sample(label = "Hello", republic, lab)]
        struct Demo {}
    })
    .map(|_| "Should have failed")
    .unwrap_err();

    assert_eq!(errors.len(), 2);
    let mut errors = errors.into_iter();
    assert_error_has_suggestion(
        &errors.next().expect("length is 2, so first `next` returns"),
        "public",
    );
    assert_error_has_suggestion(
        &errors
            .next()
            .expect("length is 2, so second `next` returns"),
        "label",
    );
}

/// Make sure that flatten works with smart pointer types, e.g. `Box`.
///
/// The generated `flatten` impl directly calls `FromMeta::from_list`
/// rather than calling `from_meta`, and the default impl of `from_list`
/// will return an unsupported format error; this test ensures that the
/// smart pointer type is properly forwarding the `from_list` call.
#[test]
fn flattening_to_box() {
    #[derive(FromDeriveInput)]
    #[darling(attributes(v))]
    struct Example {
        #[darling(flatten)]
        items: Box<Vis>,
    }

    let when_omitted = Example::from_derive_input(&parse_quote! {
        struct Demo;
    })
    .unwrap();

    assert!(!when_omitted.items.public.is_present());
}
