//! A macro for running `async fn` tests.
//!
//! Runs tests with [`safina::block_on`](https://docs.rs/safina/latest/safina/fn.block_on.html).
//!
//! Related crates:
//! - [`safina`](https://crates.io/crates/safina) - safe async runtime
//! - [`safina-timer`](https://crates.io/crates/safina-timer) - safe async sleep functions
//! - [`safina-async-test`](https://crates.io/crates/safina-async-test) - macro for running async tests
//!
//! # Limitations
//! - Requires Rust `nightly` because safina does
//!
//! # Examples
//! ```rust
//! # use safina_async_test::async_test;
//! # async fn an_async_fn() -> Result<(), std::io::Error> { Ok(()) }
//! #[async_test]
//! async fn test1() {
//!     an_async_fn().await.unwrap();
//! }
//! ```
//!
//! ```rust
//! # use safina_async_test::async_test;
//! # use std::boxed::Box;
//! # async fn background_task() {}
//! # async fn do_request() -> Result<u8, std::io::Error> { Ok(42) }
//! #[async_test]
//! async fn test2() {
//!     safina::increase_threads_to(1);
//!     safina::spawn(Box::pin(background_task()));
//!     assert_eq!(42, do_request().await.unwrap());
//! }
//! ```
//!
//! # Documentation
//! https://docs.rs/safina-async-test
//!
//! # Alternatives
//! - [`async_std::test`](https://docs.rs/async-std/latest/async_std/attr.test.html)
//! - [`futures_await_test::async_test`](https://docs.rs/futures-await-test/0.3.0/futures_await_test/)
//! - [`tokio::test`](https://docs.rs/tokio/latest/tokio/attr.test.html)
//!
//! # Release Process
//! 1. Edit `Cargo.toml` and bump version number.
//! 1. Run `./release.sh`
//!
//! # Changelog
//! - v0.1.1 - First published version
//!
//! # TO DO
//! - DONE - Implement as declarative macro.  UX is bad.
//! - DONE - Implement as procedural macro.
//! - DONE - Report errors nicely
//! - DONE - Drop finished futures
//! - Publish on crates.io
#![forbid(unsafe_code)]

use proc_macro;
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::quote_spanned;

#[proc_macro_attribute]
pub fn async_test(
    attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let mut result = proc_macro::TokenStream::from(impl_async_test(
        proc_macro2::TokenStream::from(attr.clone()),
        proc_macro2::TokenStream::from(item.clone()),
    ));
    result.extend(item);

    // let mut token_stream_string = String::new();
    // for tree in result {
    //     token_stream_string.push_str(format!("tree: {:?}\n", tree).as_str());
    // }
    // panic!("TokenStream:\n{}", token_stream_string);

    result
}

fn impl_async_test(attr: TokenStream, item: TokenStream) -> TokenStream {
    for tree in attr {
        return quote_spanned!(tree.span()=>compile_error!("parameters not allowed"););
    }
    // Ident { ident: "async", span: #0 bytes(50..55) }
    // Ident { ident: "fn", span: #0 bytes(56..58) }
    // Ident { ident: "should_run_async_fn", span: #0 bytes(59..78) }
    // Group { delimiter: Parenthesis, stream: TokenStream [], span: ...
    // Group { delimiter: Brace, stream: TokenStream [Ident { ident: "println",...
    let mut iter = item.into_iter();
    let first = iter.next();
    if let (
        Some(TokenTree::Ident(ref ident_async)),
        Some(TokenTree::Ident(ref ident_fn)),
        Some(TokenTree::Ident(ref ident_name)),
    ) = (&first, iter.next(), iter.next())
    {
        if ident_async.to_string() == "async" && ident_fn.to_string() == "fn" {
            let async_name = ident_name.to_string() + "_";
            let ident_async_name = Ident::new(&async_name, ident_name.span());
            return quote_spanned!(ident_name.span()=>
                #[test]
                pub fn #ident_async_name () {
                    safina::block_on(std::boxed::Box::pin( #ident_name ()))
                }
            );
        }
    }
    if let Some(tree) = first {
        return quote_spanned!(tree.span()=>compile_error!("expected async fn"););
    } else {
        panic!("expected async fn");
    }
}

// The version below has two drawbacks:
// 1. IDEs don't know that the stuff inside `async_test! { async fn test1() { ... } }` is a
//    function, so they don't provide formatting, auto-complete, or other useful editor functions.
//    This is a fatal flaw.
// 2. It puts the test in a module with the same name as the test function,
//    and a test function named `async_test`.
//    So for `async_test! { async fn test1() {...} }` below, it makes
//    `mod test1 { #[test] fn async_test() {...} }`.
//    This makes IDE test output more verbose.  Each test appears as its own category.
//
// macro_rules! async_test {
//     (async fn $name:ident () $body:block) => {
//         async fn $name() $body
//
//         mod $name {
//             #[test]
//             fn async_test () {
//                 safina::block_on(std::boxed::Box::pin(super::$name()))
//             }
//         }
//     }
// }
//
// async_test! {
// async fn test1() {
//     println!("test1");
// }
// }

// The version below requires the user to keep the two names in sync.
// Also, the error messages are not great.
//
// macro_rules! async_test {
//     ($name:ident) => {
//         mod $name {
//             #[test]
//             fn async_test () {
//                 safina::block_on(std::boxed::Box::pin(super::$name()))
//             }
//         }
//     }
// }
//
// async_test!(test1);
// async fn test1() {
//     println!("test1");
// }
//
// async_test!(test1); // error[E0428]: the name `test1` is defined multiple times
// async fn test2() {
//     println!("test2");
// }
//
// async_test!(test3); // error[E0423]: expected function, found module `super::test3`
// async fn test4() {
//     println!("test4");
// }
