//! This crate provides macros for testing procedural macros.
//!
//! Only attribute macros are supported at the moment.

/// Asserts that the first code block with attribute macro
/// expands to the second code block.
///
/// The first code block must consist of
/// a attribute macro and an item.
/// The implementation function of the macro must
/// * present in the same scope with the same name as the attribute macro
/// * have a signature of `(macro_attributes: A, applied_item: I) -> Result<R, _>`,
///   where `A` and `I` implement [`From<proc_macro2::TokenStream>`](proc_macro2::TokenStream),
///   and `R` implements [`Into<proc_macro2::TokenStream>`](proc_macro2::TokenStream)
///
/// The second code block may consist of arbitrary tokens.
///
/// See also [`assert_yields!`] for macros
/// that does not modify the original input.
///
/// # Examples
/// ```
/// use proc_macro_tester::assert_expands;
///
/// assert_expands!(
///     {
///         #[add_id_field(u32)]
///         struct Person {
///             name: String,
///         }
///     },
///     {
///         struct Person {
///             id: u32,
///             name: String,
///         }
///     }
/// );
///
/// # // macro implementation
/// # // usage: #[add_id_field(id_type)]
/// # use syn::spanned::Spanned;
/// # fn add_id_field(
/// #     attrs: proc_macro2::TokenStream,
/// #     item: proc_macro2::TokenStream,
/// # ) -> syn::Result<proc_macro2::TokenStream> {
/// #     // get id type from macro attributes
/// #     let id_type: syn::Type = syn::parse2(attrs)?;
/// #     // get input struct
/// #     let mut item_struct: syn::ItemStruct = syn::parse2(item)?;
/// #
/// #     // get struct fields
/// #     let syn::Fields::Named(fields) = &mut item_struct.fields else {
/// #         return Err(syn::Error::new(
/// #             item_struct.fields.span(),
/// #             "The struct must have named fields",
/// #         ));
/// #     };
/// #     // prepend an id field
/// #     fields.named.insert(
/// #         0,
/// #         syn::parse_quote! {
/// #           id: #id_type
/// #         },
/// #     );
/// #
/// #     // return the modified struct
/// #     Ok(quote::quote! {
/// #         #item_struct
/// #     })
/// # }
/// ```
#[macro_export]
macro_rules! assert_expands {
    (
        {
            #[ $macro_name:ident $(( $($attrs:tt)* ))? ]
            $item:item
        },
        {
            $($expanded:tt)*
        }
    ) => {
        ::core::assert_eq!(
            ::std::string::ToString::to_string(
                &$macro_name(
                    ::core::convert::Into::into(::quote::quote!($( $($attrs)* )?)),
                    ::core::convert::Into::into(::quote::quote!($item))
                ).unwrap()
            ),
            ::std::string::ToString::to_string(
                &::quote::quote!($($expanded)*)
            )
        );
    };
    (
        {
            #[ $(::)? $_:ident :: $($macro_path:ident)::* $(( $($attrs:tt)* ))? ]
            $item:item
        },
        {
            $($expanded:tt)*
        }
    ) => {
        $crate::assert_expands!(
            {
                #[ $($macro_path)::* $(( $($attrs)* ))? ]
                $item
            },
            {
                $($expanded)*
            }
        );
    };
}

/// Asserts that the first code block with attribute macro
/// yields the second code block.
///
/// The first code block must consist of
/// a attribute macro and an item.
/// The implementation function of the macro must
/// * present in the same scope with the same name as the attribute macro
/// * have a signature of `(macro_attributes: A, applied_item: I) -> Result<R, _>`,
///   where `A` and `I` implement [`From<proc_macro2::TokenStream>`](proc_macro2::TokenStream),
///   and `R` implements [`Into<proc_macro2::TokenStream>`](proc_macro2::TokenStream)
///
/// The second code block may consist of arbitrary tokens.
///
/// See also [`assert_expands!`] for macros
/// that modify the original input.
///
/// # Examples
/// ```
/// use proc_macro_tester::assert_yields;
///
/// assert_yields!(
///     {
///         #[create_struct_without_id(NewPerson)]
///         struct Person {
///             id: u32,
///             name: String,
///         }
///     },
///     {
///         // do not write the original item here
///         // struct Person {
///         //     id: u32,
///         //     name: String,
///         // }
///         struct NewPerson {
///             name: String,
///         }
///     }
/// );
///
/// # // macro implementation
/// # // usage: #[struct_without_id(NewStructName)]
/// # use syn::spanned::Spanned;
/// # fn create_struct_without_id(
/// #     attrs: proc_macro2::TokenStream,
/// #     item: proc_macro2::TokenStream,
/// # ) -> syn::Result<proc_macro2::TokenStream> {
/// #     // get new struct name from macro attributes
/// #     let struct_name: syn::Ident = syn::parse2(attrs)?;
/// #     // get input struct
/// #     let item_struct: syn::ItemStruct = syn::parse2(item)?;
/// #
/// #     // get struct fields
/// #     let syn::Fields::Named(fields_named_orig) = &item_struct.fields else {
/// #         return Err(syn::Error::new(
/// #             item_struct.fields.span(),
/// #             "The struct must have named fields",
/// #         ));
/// #     };
/// #     // create fields for new struct without id field
/// #     let mut found_id = false;
/// #     let fields_new = fields_named_orig
/// #         .named
/// #         .clone()
/// #         .into_pairs()
/// #         .filter(|punct_field| match punct_field.value().ident {
/// #             Some(ref ident) if ident == "id" => {
/// #                 found_id = true;
/// #                 false
/// #             }
/// #             _ => true,
/// #         })
/// #         .collect();
/// #     if !found_id {
/// #         return Err(syn::Error::new(
/// #             item_struct.fields.span(),
/// #             "The struct does not have an `id` field",
/// #         ));
/// #     }
/// #
/// #     // generated struct
/// #     let new_struct = syn::ItemStruct {
/// #         ident: struct_name,
/// #         fields: syn::Fields::Named(syn::FieldsNamed {
/// #             brace_token: fields_named_orig.brace_token,
/// #             named: fields_new,
/// #         }),
/// #         ..item_struct.clone()
/// #     };
/// #
/// #     // return the modified struct
/// #     Ok(quote::quote! {
/// #         #item_struct
/// #         #new_struct
/// #     })
/// # }
/// ```
#[macro_export]
macro_rules! assert_yields {
    ({
        #[ $(::)? $macro_path1:ident $(:: $macro_path2:ident)* $(( $($attrs:tt)* ))? ]
        $item:item
    }, {
        $($expanded:tt)*
    }) => {
        $crate::assert_expands!(
            {
                #[ $macro_path1 $(:: $macro_path2)* $(( $($attrs)* ))? ]
                $item
            },
            {
                $item
                $($expanded)*
            }
        );
    };
}

#[cfg(test)]
mod unit_test {
    use super::*;

    fn macro_debugger(
        attrs: proc_macro2::TokenStream,
        item: proc_macro2::TokenStream,
    ) -> Result<proc_macro2::TokenStream, ()> {
        Ok(quote::quote! {
            attrs = { #attrs }
            item = { #item }
        })
    }

    fn thrice(
        _attrs: proc_macro2::TokenStream,
        item: proc_macro2::TokenStream,
    ) -> Result<proc_macro2::TokenStream, ()> {
        Ok(quote::quote! {
            #item
            #item
            #item
        })
    }

    fn identity_macro(
        _attrs: proc_macro2::TokenStream,
        item: proc_macro2::TokenStream,
    ) -> Result<proc_macro2::TokenStream, ()> {
        Ok(item)
    }

    #[test]
    fn assert_expands() {
        assert_expands!(
            {
                #[macro_debugger(any token here (as attrs))]
                struct Some(Item);
            },
            {
                attrs = {
                    any token here (as attrs)
                }
                item = {
                    struct Some(Item);
                }
            }
        );
        assert_expands!(
            {
                #[thrice(ignored attrs)]
                some_item!();
            },
            {
                some_item!();
                some_item!();
                some_item!();
            }
        );
        assert_expands!(
            {
                #[identity_macro(ignored attrs)]
                mod some_item;
            },
            {
                mod some_item;
            }
        );
    }

    #[test]
    fn assert_yields() {
        assert_yields!(
            {
                #[thrice(ignored attrs)]
                some_item!();
            },
            {
                some_item!();
                some_item!();
            }
        );
        assert_yields!(
            {
                #[identity_macro(ignored attrs)]
                mod some_item;
            },
            {
            }
        );
    }

    #[test]
    fn multiple_segments_macro_path() {
        assert_expands!(
            {
                #[path::to::identity_macro(ignored attrs)]
                mod some_item;
            },
            {
                mod some_item;
            }
        );

        assert_yields!(
            {
                #[path::to::identity_macro(ignored attrs)]
                mod some_item;
            },
            {
            }
        );
    }

    #[test]
    fn macro_path_with_leading_colons() {
        assert_expands!(
            {
                #[::path::to::identity_macro(ignored attrs)]
                mod some_item;
            },
            {
                mod some_item;
            }
        );

        assert_yields!(
            {
                #[::path::to::identity_macro(ignored attrs)]
                mod some_item;
            },
            {
            }
        );
    }
}
