#[allow(dead_code)]
#[path = "printers/array/mod.rs"]
mod array;

#[allow(dead_code)]
#[path = "printers/attribute/mod.rs"]
mod attribute;

#[allow(dead_code)]
#[path = "printers/enum/mod.rs"]
mod r#enum;

#[allow(dead_code)]
#[path = "printers/features/mod.rs"]
mod features;

#[allow(dead_code)]
#[path = "printers/formatting/mod.rs"]
mod formatting;

#[allow(dead_code)]
#[path = "printers/map/mod.rs"]
mod map;

#[allow(dead_code)]
#[path = "printers/metas/mod.rs"]
mod metas;

#[allow(dead_code)]
#[path = "printers/optional/mod.rs"]
mod optional;

#[allow(dead_code)]
#[path = "printers/struct/mod.rs"]
mod r#struct;

#[allow(dead_code)]
#[path = "printers/tuple/mod.rs"]
mod tuple;

// ---

mod prelude {
    #![allow(clippy::single_component_path_imports)]

    pub use super::*;
    pub use doku::Document;
    pub use serde::{Deserialize, Serialize};

    macro_rules! printer_test {
        (
            $(
                $file:literal => $assert_fn:ident $assert_args:tt
            ),+
            $(,)?
        ) => {
            #[test]
            fn test() {
                let mut expectations = Vec::new();

                $(
                    let expected = printer_test!(@assert $assert_fn $assert_args);
                    expectations.push(($file, expected));
                )+

                use std::path::{Path, PathBuf};

                let dir: PathBuf = Path::new(file!())
                    .parent()
                    .unwrap()
                    .iter()
                    .skip(1)
                    .collect();

                assert_dir(dir, expectations);
            }
        };

        (@assert to_json($ty:ty)) => {{
            doku::to_json::<$ty>()
        }};

        (@assert to_json_without_comma($ty:ty)) => {{
            printer_test!(@assert to_json_without_comma($ty, {}))
        }};

        (@assert to_json_without_comma($ty:ty, $fmt:tt)) => {{
            let fmt = serde_json::json!($fmt);
            let mut fmt: doku::json::Formatting = serde_json::from_value(fmt).expect("Given formatting is not valid");
            fmt.objects_style.use_comma_as_separator = false;

            doku::to_json_fmt::<$ty>(&fmt)
        }};

        (@assert to_json_without_key_quotes($ty:ty)) => {{
            printer_test!(@assert to_json_without_key_quotes($ty, {}))
        }};

        (@assert to_json_without_key_quotes($ty:ty, $fmt:tt)) => {{
            let fmt = serde_json::json!($fmt);
            let mut fmt: doku::json::Formatting = serde_json::from_value(fmt).expect("Given formatting is not valid");
            fmt.objects_style.surround_keys_with_quotes = false;

            doku::to_json_fmt::<$ty>(&fmt)
        }};

        (@assert to_json_fmt($ty:ty, $fmt:tt)) => {{
            let fmt = serde_json::json!($fmt);
            let fmt = serde_json::from_value(fmt).expect("Given formatting is not valid");

            doku::to_json_fmt::<$ty>(&fmt)
        }};

        (@assert to_json_fmt_val($ty:ty, $fmt:tt)) => {{
            let fmt = serde_json::json!($fmt);
            let fmt = serde_json::from_value(fmt).expect("Given formatting is not valid");

            doku::to_json_fmt_val(&fmt, &<$ty>::default())
        }};

        (@assert to_json_val($ty:ty)) => {{
            doku::to_json_val(&<$ty>::default())
        }};

        (@assert to_json_val_without_comma($ty:ty)) => {{
            let mut fmt = doku::json::Formatting::default();
            fmt.objects_style.use_comma_as_separator = false;

            doku::to_json_fmt_val(&fmt, &<$ty>::default())
        }};

        (@assert to_json_val_without_key_quotes($ty:ty)) => {{
            let mut fmt = doku::json::Formatting::default();
            fmt.objects_style.surround_keys_with_quotes = false;

            doku::to_json_fmt_val(&fmt, &<$ty>::default())
        }};

        (@assert to_toml($ty:ty)) => {{
            doku::to_toml::<$ty>()
        }};

        (@assert to_toml_fmt($ty:ty, $fmt:tt)) => {{
            let fmt = serde_json::json!($fmt);
            let fmt = serde_json::from_value(fmt).expect("Given formatting is not valid");

            doku::to_toml_fmt::<$ty>(&fmt)
        }};

        (@assert to_toml_fmt_val($ty:ty, $fmt:tt)) => {{
            let fmt = serde_toml::toml!($fmt);
            let fmt = serde_toml::from_value(fmt).expect("Given formatting is not valid");

            doku::to_toml_fmt_val(&fmt, &<$ty>::default())
        }};

        (@assert to_toml_val($ty:ty)) => {{
            doku::to_toml_val(&<$ty>::default())
        }};
    }

    macro_rules! panic_test {
        (
            $message:literal => $assert_fn:ident $assert_args:tt
        ) => {
            panic_test!{test: $message => $assert_fn $assert_args}
        };

        (
            $($mod_name:ident: $message:literal => $assert_fn:ident $assert_args:tt),+
            $(,)?
        ) => {
            $(mod $mod_name {
                use super::*;

                #[test]
                #[should_panic = $message]
                fn panic_test() {
                    printer_test!(@assert $assert_fn $assert_args);
                }
            })+
        };
    }

    #[derive(Document)]
    #[allow(dead_code)]
    pub(crate) struct TomlWrapper<T> {
        inner: T,
    }

    pub(crate) use panic_test;
    pub(crate) use printer_test;
}

use difference::Changeset;
use std::fs;
use std::path::Path;

type FileName = &'static str;
type FileBody = String;
type DidAssertSucceed = bool;

pub fn assert_dir(
    dir: impl AsRef<Path>,
    expectations: Vec<(FileName, FileBody)>,
) {
    let dir = dir.as_ref();
    let mut all_asserts_succeeded = true;

    for (file, expected) in expectations {
        all_asserts_succeeded &= assert(dir, file, expected);
    }

    if !all_asserts_succeeded {
        panic!("Some assertions failed");
    }
}

fn assert(dir: &Path, file: FileName, expected: FileBody) -> DidAssertSucceed {
    let path = dir.join(file);

    let path_new = path.with_extension(format!(
        "{}.new",
        path.extension().unwrap().to_string_lossy()
    ));

    if path_new.exists() {
        fs::remove_file(&path_new).unwrap_or_else(|err| {
            panic!(
                "Couldn't remove new-fixture `{}`: {}",
                path_new.display(),
                err
            )
        })
    }

    let actual = if path.exists() {
        fs::read_to_string(&path).unwrap_or_else(|err| {
            panic!("Couldn't read fixture `{}`: {}", path.display(), err)
        })
    } else {
        Default::default()
    };

    if actual == expected {
        true
    } else {
        fs::write(&path_new, &expected).unwrap_or_else(|err| {
            panic!("Couldn't write fixture `{}`: {}", path_new.display(), err)
        });

        eprintln!(
            "\nFound differences between `{}` and `{}`:\n{}",
            path.display(),
            path_new.display(),
            Changeset::new(&actual, &expected, "\n"),
        );

        false
    }
}
