mod format;
pub mod formatter;
mod types;

use format::Format;
pub use formatter::Formatter;
pub use formatter::definitions::FormatDefinitions;
pub use tombi_config::FormatOptions;

#[macro_export]
macro_rules! test_format {
    // Main entry point: Ok(source) means expected equals source
    {
        #[tokio::test]
        async fn $name:ident($source:expr $(, $arg:expr )* $(,)? ) -> Ok(source)
    } => {
        $crate::test_format! {
            #[tokio::test]
            async fn $name($source $(, $arg)*) -> Ok($source)
        }
    };

    // Main entry point: Ok(expected) with custom expected value
    {
        #[tokio::test]
        async fn $name:ident($source:expr $(, $arg:expr )* $(,)? ) -> Ok($expected:expr)
    } => {
        #[tokio::test]
        async fn $name() {
            use textwrap::dedent;
            use tombi_schema_store::SchemaStore;
            use tombi_config::{TomlVersion, FormatOptions};


            tombi_test_lib::init_tracing();

            /// Test-time configuration overridden via `test_format!` arguments.
            #[allow(unused)]
            #[derive(Default)]
            pub struct TestConfig {
                pub toml_version: TomlVersion,
                pub options: FormatOptions,
                pub schema_path: Option<std::path::PathBuf>,
            }

            #[allow(unused)]
            pub trait ApplyTestArg {
                fn apply(self, config: &mut TestConfig);
            }

            impl ApplyTestArg for TomlVersion {
                fn apply(self, config: &mut TestConfig) {
                    config.toml_version = self;
                }
            }

            impl ApplyTestArg for FormatOptions {
                fn apply(self, config: &mut TestConfig) {
                    config.options = self;
                }
            }

            /// Set schema path for the test case.
            #[allow(unused)]
            pub struct SchemaPath(pub std::path::PathBuf);

            impl ApplyTestArg for SchemaPath {
                fn apply(self, config: &mut TestConfig) {
                    config.schema_path = Some(self.0);
                }
            }

            #[allow(unused_mut)]
            let mut config = TestConfig::default();
            $(
                ApplyTestArg::apply($arg, &mut config);
            )*

            // Initialize schema store
            let schema_store = SchemaStore::new();

            if let Some(schema_path) = &config.schema_path {
                let path = tombi_uri::Uri::from_file_path(schema_path.as_path())
                    .unwrap()
                    .to_string();
                // Load schemas
                schema_store
                    .load_config_schemas(
                        &[tombi_config::SchemaItem::Root(tombi_config::RootSchema {
                            toml_version: None,
                            path,
                            include: vec!["*.toml".to_string()],
                        })],
                        None,
                    )
                    .await;
            }

            // Initialize formatter
            let source_path = tombi_test_lib::project_root_path().join("test.toml");
            let formatter = Formatter::new(
                config.toml_version,
                &config.options,
                Some(itertools::Either::Right(source_path.as_path())),
                &schema_store,
            );

            // Test that keys are reordered according to schema order
            let source = dedent($source).to_string();
            let mut expected = dedent($expected).trim().to_string();
            if !source.trim().is_empty() {
                expected.push('\n');
            }

            let formatted = formatter.format(&source).await.unwrap();
            pretty_assertions::assert_eq!(
                formatted,
                expected,
                "Formatting should be equal to expected"
            );
        }
    };

    // Main entry point: Err(_) for error cases
    {
        #[tokio::test]
        async fn $name:ident($source:expr $(, $arg:expr )* $(,)? ) -> Err(_)
    } => {
        #[tokio::test]
        async fn $name() {
            use tombi_schema_store::SchemaStore;
            use tombi_config::{FormatOptions, TomlVersion};


            tombi_test_lib::init_tracing();

            /// Test-time configuration overridden via `test_format!` arguments.
            #[allow(unused)]
            #[derive(Default)]
            pub struct TestConfig {
                pub toml_version: TomlVersion,
                pub options: FormatOptions,
                pub schema_path: Option<std::path::PathBuf>,
            }

            #[allow(unused)]
            pub trait ApplyTestArg {
                fn apply(self, config: &mut TestConfig);
            }

            impl ApplyTestArg for TomlVersion {
                fn apply(self, config: &mut TestConfig) {
                    config.toml_version = self;
                }
            }

            impl ApplyTestArg for FormatOptions {
                fn apply(self, config: &mut TestConfig) {
                    config.options = self;
                }
            }

            /// Set schema path for the test case.
            #[allow(unused)]
            pub struct SchemaPath(pub std::path::PathBuf);

            impl ApplyTestArg for SchemaPath {
                fn apply(self, config: &mut TestConfig) {
                    config.schema_path = Some(self.0);
                }
            }

            #[allow(unused_mut)]
            let mut config = TestConfig::default();
            $(
                ApplyTestArg::apply($arg, &mut config);
            )*

            // Initialize schema store
            let schema_store = SchemaStore::new();

            if let Some(schema_path) = config.schema_path {
                let path = tombi_uri::Uri::from_file_path(schema_path)
                    .unwrap()
                    .to_string();
                // Load schemas
                schema_store
                    .load_config_schemas(
                        &[tombi_config::SchemaItem::Root(tombi_config::RootSchema {
                            toml_version: None,
                            path,
                            include: vec!["*.toml".to_string()],
                            })],
                        None,
                    )
                    .await;
            }

            // Initialize formatter
            let source_path = tombi_test_lib::project_root_path().join("test.toml");
            let formatter = Formatter::new(
                config.toml_version,
                &config.options,
                Some(itertools::Either::Right(source_path.as_path())),
                &schema_store,
            );

            // Test that keys are reordered according to schema order
            let source = textwrap::dedent($source).trim().to_string();
            match formatter.format(&source).await {
                Ok(_) => panic!("Expected error, got success"),
                Err(errors) => {
                    pretty_assertions::assert_ne!(errors, vec![]);
                }
            }
        }
    };
}

#[cfg(test)]
mod test {
    use super::*;
    use tombi_config::{StringQuoteStyle, format::FormatRules};

    test_format! {
        #[tokio::test]
        async fn test_empty(
            r#""#,
            TomlVersion::V1_0_0
        ) -> Ok(source)
    }

    test_format! {
        #[tokio::test]
        async fn test_whitespace(
            r#"    "#,
            TomlVersion::V1_0_0
        ) -> Ok("")
    }

    test_format! {
        #[tokio::test]
        async fn test_key_values(r#"
            array5 = [
              1,
              {
                # inline begin dangling comment1
                # inline begin dangling comment2

                # key1 leading comment1
                # key1 leading comment2
                key1 = 1,  # key1 trailing comment
                # key2 leading comment1
                key2 = 2,  # key2 trailing comment

                # inline end dangling comment1
                # inline end dangling comment2
              },

              # comment
            ]
            "#,
            TomlVersion::V1_1_0
        ) -> Ok(source)
    }

    test_format! {
        #[tokio::test]
        async fn test_sample_toml(
r#"
# key values begin dangling comment1
# key values begin dangling comment2

# key values begin dangling comment3
# key values begin dangling comment4

key1 = 1
key2 = "2"

# key values end dangling comment1
# key values end dangling comment2

# key values end dangling comment3
# key values end dangling comment4

# table leading comment1
# table leading comment2
[aaaa]
# table leading comment1
# table leading comment2
[aaaa.bbb]
bool1 = true
bool2 = false
dec = 1  # dec trailing comment
bin = 0b01  # bin trailing comment
oct = 0o01  # oct trailing comment
hex = 0x01  # hex trailing comment
float1 = 0.1234  # float trailing comment
infa = inf
bs = "2"  # bs trailing comment
ls = '3'  # ls trailing comment
array1 = [
  # array begin dangling comment1
  # array begin dangling comment2

  # value1 leading comment1
  # value1 leading comment2
  # value1 leading comment3
  { key3 = 12, key4 = 2024-01-01T10:10:00 }
  # value1 comma leading comment1
  # value1 comma leading comment2
  # value1 comma leading comment3
  ,  # value1 comma trailing comment
  { key3 = 11, key4 = 2024-01-01T10:10:00 },

  # array end dangling comment1
  # array end dangling comment2
]  # array trailing comment
array2 = [1, 2, 3]
array3 = [
  1,
  2,
  3,
]
array4 = [
  [
    1,
    2,
    3,
  ],
  [1, 2, 3],
]
array5 = [
  1,
  {
    # inline begin dangling comment1
    # inline begin dangling comment2

    # key1 leading comment1
    # key1 leading comment2
    key1 = 1,  # key1 trailing comment
    # key2 leading comment1
    key2 = 2,  # key2 trailing comment

    # inline end dangling comment1
    # inline end dangling comment2
  },

  # comment
]
date = 2024-01-01  # date trailing comment
time = 10:11:00  # time trailing comment
odt1 = 1979-05-27T07:32:00Z  # odt1 trailing comment
odt2 = 1979-05-27T00:32:00-07:00  # odt2 trailing comment
odt3 = 1979-05-27T00:32:00.999999-07:00  # odt3 trailing comment
odt4 = 1979-05-27T07:32:00Z  # odt4 trailing comment
ldt1 = 1979-05-27T07:32:00  # ldt1 trailing comment
ldt2 = 1979-05-27T00:32:00.999999  # ldt2 trailing comment
ld1 = 1979-05-27  # ld1 trailing comment
lt1 = 07:32:00  # lt1 trailing comment
# lt2 leading trailing comment
lt2 = 00:32:00.999999  # lt2 trailing comment

# table key values end dangling comment1
# table key values end dangling comment2

# table key values end dangling comment3
# table key values end dangling comment4

# table leading comment1
# table leading comment2
[aaaa.ccc]
key1 = 11
key2 = "22"

[bbb]
key5 = true

[ddd.eee]  # header trailing comment
key5 = true

[[ffff.ggg]]
key6 = 1

[[ffff.ggg]]
key6 = 2

[[ffff.ggg]]  # header trailing comment
# table key values begin dangling comment1
# table key values begin dangling comment2

# table key values begin dangling comment3
# table key values begin dangling comment4

# key value leading comment1
# key value leading comment2
key6 = 3  # key value trailing comment

[ffff.ggg.kkk]
b = 3

# table key values end dangling comment1
# table key values end dangling comment2

# table key values end dangling comment3
# table key values end dangling comment4
"#,
        TomlVersion::V1_1_0,
        FormatOptions {
            rules: Some(FormatRules {
                string_quote_style: Some(StringQuoteStyle::Preserve),
                ..Default::default()
            }),
        }
    ) -> Ok(source)
    }
}
