mod utils;

use moon_config::*;
use rustc_hash::FxHashMap;
use serde_json::Value as JsonValue;
use std::path::Path;
use utils::*;

fn load_config_from_root(root: &Path) -> miette::Result<TemplateConfig> {
    ConfigLoader::new(root.join(".moon")).load_template_config(root)
}

mod template_config {
    use super::*;

    #[test]
    #[should_panic(
        expected = "unknown field `unknown`, expected one of `$schema`, `description`, `destination`, `extends`, `id`, `title`, `variables`"
    )]
    fn error_unknown_field() {
        test_load_config("template.yml", "unknown: 123", |path| {
            load_config_from_root(path)
        });
    }

    #[test]
    fn loads_defaults() {
        let config = test_load_config(
            "template.yml",
            "title: title\ndescription: description",
            load_config_from_root,
        );

        assert_eq!(config.title, "title");
        assert_eq!(config.description, "description");
        assert_eq!(config.variables, FxHashMap::default());
    }

    mod title {
        use super::*;

        #[test]
        #[should_panic(expected = "invalid type: integer `123`, expected a string")]
        fn invalid_type() {
            test_load_config("template.yml", "title: 123", |path| {
                load_config_from_root(path)
            });
        }

        #[test]
        #[should_panic(expected = "title: must not be empty")]
        fn not_empty() {
            test_load_config("template.yml", "title: ''\ndescription: 'asd'", |path| {
                load_config_from_root(path)
            });
        }
    }

    mod description {
        use super::*;

        #[test]
        #[should_panic(expected = "invalid type: integer `123`, expected a string")]
        fn invalid_type() {
            test_load_config("template.yml", "description: 123", |path| {
                load_config_from_root(path)
            });
        }

        #[test]
        #[should_panic(expected = "description: must not be empty")]
        fn not_empty() {
            test_load_config("template.yml", "title: 'asd'\ndescription: ''", |path| {
                load_config_from_root(path)
            });
        }
    }

    mod variables {
        use super::*;

        #[test]
        #[should_panic(
            expected = "unknown variant `other`, expected one of `array`, `boolean`, `enum`, `number`, `object`, `string`"
        )]
        fn error_unknown_variable_type() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  unknown:
    type: other
",
                load_config_from_root,
            );
        }

        #[test]
        fn loads_array() {
            let config = test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  array:
    type: array
    default: [abc, true]
    prompt: prompt
    required: true
",
                load_config_from_root,
            );

            assert_eq!(
                *config.variables.get("array").unwrap(),
                TemplateVariable::Array(TemplateVariableArraySetting {
                    default: vec![JsonValue::String("abc".into()), JsonValue::Bool(true)],
                    internal: false,
                    order: None,
                    prompt: Some("prompt".into()),
                    required: Some(true)
                })
            );
        }

        #[test]
        #[should_panic(expected = "invalid type: integer `123`, expected a sequence")]
        fn invalid_array() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  array:
    type: array
    default: 123
",
                load_config_from_root,
            );
        }

        #[test]
        fn loads_boolean() {
            let config = test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  bool:
    type: boolean
    default: false
    prompt: prompt
    required: true
",
                load_config_from_root,
            );

            assert_eq!(
                *config.variables.get("bool").unwrap(),
                TemplateVariable::Boolean(TemplateVariableBoolSetting {
                    default: false,
                    internal: false,
                    order: None,
                    prompt: Some("prompt".into()),
                    required: Some(true)
                })
            );
        }

        #[test]
        #[should_panic(expected = "invalid type: integer `123`, expected a boolean")]
        fn invalid_boolean() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  bool:
    type: boolean
    default: 123
",
                load_config_from_root,
            );
        }

        #[test]
        fn loads_number() {
            let config = test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  num:
    type: number
    default: 123
    prompt: prompt
    required: false
",
                load_config_from_root,
            );

            assert_eq!(
                *config.variables.get("num").unwrap(),
                TemplateVariable::Number(TemplateVariableNumberSetting {
                    default: 123,
                    internal: false,
                    order: None,
                    prompt: Some("prompt".into()),
                    required: Some(false)
                })
            );
        }

        #[test]
        #[should_panic(expected = "invalid type: boolean `true`, expected isize")]
        fn invalid_number() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  num:
    type: number
    default: true
",
                load_config_from_root,
            );
        }

        #[test]
        fn loads_object() {
            let config = test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  object:
    type: object
    default:
      str: abc
      bool: true
    prompt: prompt
    required: true
",
                load_config_from_root,
            );

            assert_eq!(
                *config.variables.get("object").unwrap(),
                TemplateVariable::Object(TemplateVariableObjectSetting {
                    default: FxHashMap::from_iter([
                        ("str".into(), JsonValue::String("abc".into())),
                        ("bool".into(), JsonValue::Bool(true)),
                    ]),
                    internal: false,
                    order: None,
                    prompt: Some("prompt".into()),
                    required: Some(true)
                })
            );
        }

        #[test]
        #[should_panic(expected = "invalid type: integer `123`, expected a map")]
        fn invalid_object() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  object:
    type: object
    default: 123
",
                load_config_from_root,
            );
        }

        #[test]
        fn loads_string() {
            let config = test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  str:
    type: string
    default: abc
",
                load_config_from_root,
            );

            assert_eq!(
                *config.variables.get("str").unwrap(),
                TemplateVariable::String(TemplateVariableStringSetting {
                    default: "abc".into(),
                    internal: false,
                    order: None,
                    prompt: None,
                    required: None
                })
            );
        }

        #[test]
        #[should_panic(expected = "invalid type: integer `123`, expected a string")]
        fn invalid_string() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  str:
    type: string
    default: 123
",
                load_config_from_root,
            );
        }

        #[test]
        fn loads_string_enum() {
            let config = test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  strum:
    type: enum
    default: a
    values:
      - a
      - b
      - label: C
        value: c
    prompt: prompt
",
                load_config_from_root,
            );

            assert_eq!(
                *config.variables.get("strum").unwrap(),
                TemplateVariable::Enum(TemplateVariableEnumSetting {
                    default: TemplateVariableEnumDefault::String("a".into()),
                    internal: false,
                    multiple: None,
                    order: None,
                    prompt: Some("prompt".into()),
                    values: vec![
                        TemplateVariableEnumValue::String("a".into()),
                        TemplateVariableEnumValue::String("b".into()),
                        TemplateVariableEnumValue::Object(TemplateVariableEnumValueConfig {
                            label: "C".into(),
                            value: "c".into()
                        })
                    ],
                })
            );
        }

        #[test]
        fn loads_string_enum_default_list() {
            let config = test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
  strum:
    type: enum
    default:
      - a
      - c
    values:
      - a
      - b
      - label: C
        value: c
    multiple: true
    prompt: prompt
",
                load_config_from_root,
            );

            assert_eq!(
                *config.variables.get("strum").unwrap(),
                TemplateVariable::Enum(TemplateVariableEnumSetting {
                    default: TemplateVariableEnumDefault::Vec(vec!["a".into(), "c".into()]),
                    internal: false,
                    multiple: Some(true),
                    order: None,
                    prompt: Some("prompt".into()),
                    values: vec![
                        TemplateVariableEnumValue::String("a".into()),
                        TemplateVariableEnumValue::String("b".into()),
                        TemplateVariableEnumValue::Object(TemplateVariableEnumValueConfig {
                            label: "C".into(),
                            value: "c".into()
                        })
                    ],
                })
            );
        }

        #[test]
        #[should_panic(expected = "expected a value string or value object with label")]
        fn invalid_string_enum() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
   strum:
    type: enum
    default: c
    values: [1, 2, 3]
    prompt: prompt
",
                load_config_from_root,
            );
        }

        #[test]
        #[should_panic(expected = "invalid default value, must be a value configured in `values`")]
        fn invalid_enum_default_value() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
   strum:
    type: enum
    default: z
    values: [a, b, c]
    prompt: prompt
",
                load_config_from_root,
            );
        }

        #[test]
        #[should_panic(
            expected = "multiple default values is not allowed unless `multiple` is enabled"
        )]
        fn errors_multi_default_when_not_multiple() {
            test_load_config(
                "template.yml",
                r"
title: title
description: description
variables:
   strum:
    type: enum
    default: [a, b]
    values: [a, b, c]
    prompt: prompt
",
                load_config_from_root,
            );
        }
    }

    #[test]
    fn supports_hcl() {
        load_template_config_in_format("hcl");
    }

    #[test]
    fn supports_pkl() {
        load_template_config_in_format("pkl");
    }

    #[test]
    fn supports_toml() {
        load_template_config_in_format("toml");
    }
}
