//! This crate is supposed to support [`json_schema_test_suite`](https://crates.io/crates/json_schema_test_suite)
//! by exporting `json_schema_test_suite` procedural macro.
//!
//! Please refer to [`json-schema-test-suite-proc-macro` docs](https://docs.rs/json-schema-test-suite) for more informaton.
#![warn(
    clippy::cast_possible_truncation,
    clippy::doc_markdown,
    clippy::explicit_iter_loop,
    clippy::match_same_arms,
    clippy::needless_borrow,
    clippy::needless_pass_by_value,
    clippy::option_map_unwrap_or,
    clippy::option_map_unwrap_or_else,
    clippy::option_unwrap_used,
    clippy::pedantic,
    clippy::print_stdout,
    clippy::redundant_closure,
    clippy::result_map_unwrap_or_else,
    clippy::result_unwrap_used,
    clippy::trivially_copy_pass_by_ref,
    missing_debug_implementations,
    missing_docs,
    trivial_casts,
    unreachable_pub,
    unsafe_code,
    unused_extern_crates,
    unused_import_braces,
    unused_qualifications,
    unused_results,
    variant_size_differences
)]
mod attribute_config;
mod mockito_mocks;
mod test_case;

use json_schema_test_suite_test_case::TestCase;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::{parse_macro_input, Ident, ItemFn};

fn test_token_stream(tests_to_ignore: &[String], wrapped_test_ident: &Ident, test: &TestCase) -> TokenStream2 {
    let name = Ident::new(&test.name, Span::call_site());

    let maybe_ignore = if tests_to_ignore.contains(&test.name.to_string()) {
        quote! { #[ignore] }
    } else {
        quote! {}
    };

    let wrapped_test_case = test_case::WrappedTestCase::from(test);
    quote! {
        #[test]
        #maybe_ignore
        fn #name() {
            setup_mocks();

            super::#wrapped_test_ident(
                &mockito::server_address().to_string(),
                #wrapped_test_case,
            );
        }
    }
}

/// Procedural macro that allows a test to be executed for all the configurations defined
/// by [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
///
/// The `proc_macro_attribute` should be used on a function with the current signature
/// ```rust
/// fn my_simple_test(
///     // address of the HTTP server providing the remote files of JSON-Schema-Test-Suite. The format will be: `hostname:port`
///     // This parameter is passed because by starting a mock server we might not start it into `localhost:1234` as expected by JSON-Schema-Test-Suite
///     server_address: &str,
///     // string representation of the draft version (equialent to the test directory)
///     // This is usefull in case your test needs to be aware of the draft version under test
///     draft_version: &str,
///     // Description of the test group as provided by JSON-Schema-Test-Suite
///     group_description: &str,
///     // Description of the test group as provided by JSON-Schema-Test-Suite
///     test_case_description: &str,
///     // Schema to be tested
///     schema: serde_json::Value,
///     // Instance to be validated against the `schema
///     instance: serde_json::Value,
///     // Expected validity status as from JSON-Schema-Test-Suite
///     expected_valid: bool
/// ) {
///     // TODO: Add here your testing logic
/// }
/// ```
#[proc_macro_attribute]
pub fn json_schema_test_suite(attr: TokenStream, item: TokenStream) -> TokenStream {
    let proc_macro_attributes = parse_macro_input!(attr as attribute_config::AttrConfig);
    let item_fn = parse_macro_input!(item as ItemFn);

    let original_function_ident = &item_fn.sig.ident;
    let tests_token_stream: Vec<TokenStream2> = test_case::load(
        &proc_macro_attributes.json_schema_test_suite_path,
        &proc_macro_attributes.draft_folder,
    )
    .iter()
    .map(|test| test_token_stream(&proc_macro_attributes.tests_to_exclude, original_function_ident, test))
    .collect();

    let setup_mockito_mocks_token_stream = mockito_mocks::setup(&proc_macro_attributes.json_schema_test_suite_path);

    let mod_name = format_ident!("{}_{}", original_function_ident, proc_macro_attributes.draft_folder);

    let output = quote! {
        #item_fn

        mod #mod_name {
            lazy_static::lazy_static! {
                static ref MOCKS: Vec<mockito::Mock> = vec![
                    #(#setup_mockito_mocks_token_stream),*
                ];
            }

            fn setup_mocks() {
                // Dereference to ensure that lazy_static actually invokes the mocks creation
                let _ = *MOCKS;
            }

            #(#tests_token_stream)*
        }
    };
    output.into()
}
