mod utils;

use moon_common::Id;
use moon_config::{
    ConfigLoader, DependencyScope, GlobPath, Input, LanguageType, LayerType, OwnersPaths,
    PortablePath, ProjectConfig, ProjectDependencyConfig, ProjectDependsOn, ProjectToolchainEntry,
    TaskArgs, ToolchainPluginConfig,
};
use proto_core::UnresolvedVersionSpec;
use rustc_hash::FxHashMap;
use schematic::schema::IndexMap;
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::Path;
use utils::*;

fn load_config_from_root(root: &Path, source: &str) -> miette::Result<ProjectConfig> {
    ConfigLoader::new(root.join(".moon")).load_project_config_from_source(root, source)
}

mod project_config {
    use super::*;

    #[test]
    #[should_panic(
        expected = "unknown field `unknown`, expected one of `$schema`, `dependsOn`, `deps`, `docker`, `env`, `fileGroups`, `id`, `language`, `layer`, `owners`, `project`, `stack`, `tags`, `tasks`, `toolchains`, `workspace`"
    )]
    fn error_unknown_field() {
        test_load_config("moon.yml", "unknown: 123", |path| {
            load_config_from_root(path, ".")
        });
    }

    #[test]
    fn loads_defaults() {
        let config = test_load_config("moon.yml", "{}", |path| load_config_from_root(path, "."));

        assert_eq!(config.language, LanguageType::Unknown);
        assert_eq!(config.layer, LayerType::Unknown);
    }

    #[test]
    fn can_use_references() {
        let config = test_load_config(
            "moon.yml",
            r"
tasks:
  build: &webpack
    command: 'webpack'
    inputs:
      - 'src/**/*'
  start:
    <<: *webpack
    args: 'serve'
",
            |path| load_config_from_root(path, "."),
        );

        let build = config.tasks.get("build").unwrap();

        assert_eq!(build.command, TaskArgs::String("webpack".to_owned()));
        assert_eq!(build.args, TaskArgs::None);
        assert_eq!(
            build.inputs,
            Some(vec![Input::Glob(stub_glob_input("src/**/*"))])
        );

        let start = config.tasks.get("start").unwrap();

        assert_eq!(start.command, TaskArgs::String("webpack".to_owned()));
        assert_eq!(start.args, TaskArgs::String("serve".to_owned()));
        assert_eq!(
            start.inputs,
            Some(vec![Input::Glob(stub_glob_input("src/**/*"))])
        );
    }

    mod depends_on {
        use super::*;

        #[test]
        fn supports_list_of_strings() {
            let config = test_load_config("moon.yml", "dependsOn: ['a', 'b', 'c']", |path| {
                load_config_from_root(path, ".")
            });

            assert_eq!(
                config.depends_on,
                vec![
                    ProjectDependsOn::String(Id::raw("a")),
                    ProjectDependsOn::String(Id::raw("b")),
                    ProjectDependsOn::String(Id::raw("c"))
                ]
            );
        }

        #[test]
        fn supports_list_of_objects() {
            let config = test_load_config(
                "moon.yml",
                r"
dependsOn:
  - id: 'a'
    scope: 'development'
  - id: 'b'
    scope: 'production'",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.depends_on,
                vec![
                    ProjectDependsOn::Object(ProjectDependencyConfig {
                        id: Id::raw("a"),
                        scope: DependencyScope::Development,
                        ..ProjectDependencyConfig::default()
                    }),
                    ProjectDependsOn::Object(ProjectDependencyConfig {
                        id: Id::raw("b"),
                        scope: DependencyScope::Production,
                        ..ProjectDependencyConfig::default()
                    })
                ]
            );
        }

        #[test]
        fn supports_list_of_strings_and_objects() {
            let config = test_load_config(
                "moon.yml",
                r"
dependsOn:
  - 'a'
  - id: 'b'
    scope: 'production'",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.depends_on,
                vec![
                    ProjectDependsOn::String(Id::raw("a")),
                    ProjectDependsOn::Object(ProjectDependencyConfig {
                        id: Id::raw("b"),
                        scope: DependencyScope::Production,
                        ..ProjectDependencyConfig::default()
                    })
                ]
            );
        }

        #[test]
        #[should_panic(expected = "expected a project identifier or dependency config object")]
        fn errors_on_invalid_object_scope() {
            test_load_config(
                "moon.yml",
                r"
dependsOn:
  - id: 'a'
    scope: 'invalid'
",
                |path| load_config_from_root(path, "."),
            );
        }
    }

    mod file_groups {
        use super::*;

        #[test]
        fn groups_into_correct_enums() {
            let config = test_load_config(
                "moon.yml",
                r"
fileGroups:
  files:
    - /ws/relative
    - proj/relative
  globs:
    - /ws/**/*
    - /!ws/**/*
    - proj/**/*
    - '!proj/**/*'
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.file_groups,
                FxHashMap::from_iter([
                    (
                        Id::raw("files"),
                        vec![
                            Input::File(stub_file_input("/ws/relative")),
                            Input::File(stub_file_input("proj/relative"))
                        ]
                    ),
                    (
                        Id::raw("globs"),
                        vec![
                            Input::Glob(stub_glob_input("/ws/**/*")),
                            Input::Glob(stub_glob_input("!/ws/**/*")),
                            Input::Glob(stub_glob_input("proj/**/*")),
                            Input::Glob(stub_glob_input("!proj/**/*")),
                        ]
                    ),
                ])
            );
        }
    }

    mod language {
        use super::*;

        #[test]
        fn supports_variant() {
            let config = test_load_config("moon.yml", "language: rust", |path| {
                load_config_from_root(path, ".")
            });

            assert_eq!(config.language, LanguageType::Rust);
        }

        #[test]
        fn unsupported_variant_becomes_other() {
            let config = test_load_config("moon.yml", "language: groovy", |path| {
                load_config_from_root(path, ".")
            });

            assert_eq!(config.language, LanguageType::Other(Id::raw("groovy")));
        }
    }

    mod owners {
        use super::*;

        #[test]
        fn loads_defaults() {
            let config = test_load_config("moon.yml", "owners: {}", |path| {
                load_config_from_root(path, ".")
            });

            assert_eq!(config.owners.custom_groups, FxHashMap::default());
            assert_eq!(config.owners.default_owner, None);
            assert!(!config.owners.optional);
            assert_eq!(config.owners.paths, OwnersPaths::List(vec![]));
            assert_eq!(config.owners.required_approvals, None);
        }

        #[test]
        fn can_set_values() {
            let config = test_load_config(
                "moon.yml",
                r"
owners:
  customGroups:
    foo: [a, b, c]
    bar: [x, y, z]
  defaultOwner: x
  optional: true
  requiredApprovals: 2
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.owners.custom_groups,
                FxHashMap::from_iter([
                    ("foo".into(), vec!["a".into(), "b".into(), "c".into()]),
                    ("bar".into(), vec!["x".into(), "y".into(), "z".into()]),
                ])
            );
            assert_eq!(config.owners.default_owner, Some("x".to_string()));
            assert!(config.owners.optional);
            assert_eq!(config.owners.required_approvals, Some(2));
        }

        #[test]
        fn can_set_paths_as_list() {
            let config = test_load_config(
                "moon.yml",
                r"
owners:
  defaultOwner: x
  paths:
    - file.txt
    - dir/**/*
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.owners.paths,
                OwnersPaths::List(vec![
                    GlobPath::parse("file.txt").unwrap(),
                    GlobPath::parse("dir/**/*").unwrap()
                ])
            );
        }

        #[test]
        fn can_set_paths_as_map() {
            let config = test_load_config(
                "moon.yml",
                r"
owners:
  paths:
    'file.txt': [a, b]
    'dir/**/*': [c, d]
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.owners.paths,
                OwnersPaths::Map(IndexMap::from_iter([
                    (
                        GlobPath::parse("file.txt").unwrap(),
                        vec!["a".into(), "b".into()]
                    ),
                    (
                        GlobPath::parse("dir/**/*").unwrap(),
                        vec!["c".into(), "d".into()]
                    ),
                ]))
            );
        }

        #[test]
        #[should_panic(expected = "a default owner is required when defining a list of paths")]
        fn errors_on_paths_list_empty_owner() {
            test_load_config(
                "moon.yml",
                r"
owners:
  paths:
    - file.txt
    - dir/**/*
",
                |path| load_config_from_root(path, "."),
            );
        }

        #[test]
        #[should_panic(
            expected = "a default owner is required when defining an empty list of owners"
        )]
        fn errors_on_paths_map_empty_owner() {
            test_load_config(
                "moon.yml",
                r"
owners:
  paths:
    'file.txt': []
",
                |path| load_config_from_root(path, "."),
            );
        }
    }

    mod project {
        use super::*;
        use serde_json::Value;

        #[test]
        fn can_set_only_description() {
            let config = test_load_config(
                "moon.yml",
                r"
project:
  description: 'Text'
",
                |path| load_config_from_root(path, "."),
            );

            let meta = config.project.unwrap();

            assert_eq!(meta.description.unwrap(), "Text");
        }

        #[test]
        fn can_set_all() {
            let config = test_load_config(
                "moon.yml",
                r"
project:
  title: Name
  description: Description
  owner: team
  maintainers: [a, b, c]
  channel: '#abc'
",
                |path| load_config_from_root(path, "."),
            );

            let meta = config.project.unwrap();

            assert_eq!(meta.title.unwrap(), "Name");
            assert_eq!(meta.description.unwrap(), "Description");
            assert_eq!(meta.owner.unwrap(), "team");
            assert_eq!(meta.maintainers, vec!["a", "b", "c"]);
            assert_eq!(meta.channel.unwrap(), "#abc");
        }

        #[test]
        fn can_set_custom_fields() {
            let config = test_load_config(
                "moon.yml",
                r"
project:
  description: 'Test'
  bool: true
  string: 'abc'
",
                |path| load_config_from_root(path, "."),
            );

            let meta = config.project.unwrap();

            assert_eq!(
                meta.metadata,
                FxHashMap::from_iter([
                    ("bool".into(), Value::Bool(true)),
                    ("string".into(), Value::String("abc".into())),
                ])
            );
        }

        #[test]
        #[should_panic(expected = "must start with a `#`")]
        fn errors_if_channel_no_hash() {
            test_load_config(
                "moon.yml",
                r"
project:
  description: Description
  channel: abc
",
                |path| load_config_from_root(path, "."),
            );
        }
    }

    mod tags {
        use super::*;

        #[test]
        fn can_set_tags() {
            let config = test_load_config(
                "moon.yml",
                r"
tags:
  - normal
  - camelCase
  - kebab-case
  - snake_case
  - dot.case
  - slash/case
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.tags,
                vec![
                    Id::raw("normal"),
                    Id::raw("camelCase"),
                    Id::raw("kebab-case"),
                    Id::raw("snake_case"),
                    Id::raw("dot.case"),
                    Id::raw("slash/case")
                ]
            );
        }

        #[test]
        #[should_panic(expected = "Invalid identifier format for `foo bar`")]
        fn errors_on_invalid_format() {
            test_load_config("moon.yml", "tags: ['foo bar']", |path| {
                load_config_from_root(path, ".")
            });
        }
    }

    mod tasks {
        use super::*;

        #[test]
        fn supports_id_patterns() {
            let config = test_load_config(
                "moon.yml",
                r"
tasks:
  normal:
    command: 'a'
  kebab-case:
    command: 'b'
  camelCase:
    command: 'c'
  snake_case:
    command: 'd'
  dot.case:
    command: 'e'
  slash/case:
    command: 'f'
",
                |path| load_config_from_root(path, "."),
            );

            assert!(config.tasks.contains_key("normal"));
            assert!(config.tasks.contains_key("kebab-case"));
            assert!(config.tasks.contains_key("camelCase"));
            assert!(config.tasks.contains_key("snake_case"));
            assert!(config.tasks.contains_key("dot.case"));
            assert!(config.tasks.contains_key("slash/case"));
        }

        #[test]
        fn can_extend_siblings() {
            let config = test_load_config(
                "moon.yml",
                r"
tasks:
  base:
    command: 'base'
  extender:
    extends: 'base'
    args: '--more'
",
                |path| load_config_from_root(path, "."),
            );

            assert!(config.tasks.contains_key("base"));
            assert!(config.tasks.contains_key("extender"));
        }
    }

    mod toolchain {
        use super::*;

        #[test]
        fn can_set_settings() {
            let config = test_load_config(
                "moon.yml",
                r"
toolchains:
  node:
    version: '18.0.0'
  typescript:
    routeOutDirToCache: true
",
                |path| load_config_from_root(path, "."),
            );

            assert!(config.toolchains.plugins.contains_key("node"));
            assert!(config.toolchains.plugins.contains_key("typescript"));

            if let ProjectToolchainEntry::Config(node) =
                config.toolchains.plugins.get("node").unwrap()
            {
                assert_eq!(
                    node.version,
                    Some(UnresolvedVersionSpec::parse("18.0.0").unwrap())
                );
            }

            if let ProjectToolchainEntry::Config(ts) =
                config.toolchains.plugins.get("typescript").unwrap()
            {
                assert_eq!(
                    ts.config.get("routeOutDirToCache").unwrap(),
                    &Value::Bool(true),
                );
            }
        }

        #[test]
        fn can_disable_with_null() {
            let config = test_load_config(
                "moon.yml",
                r"
toolchains:
    example: null
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.toolchains.plugins.get("example").unwrap(),
                &ProjectToolchainEntry::Disabled
            );
        }

        #[test]
        fn can_disable_with_false() {
            let config = test_load_config(
                "moon.yml",
                r"
toolchains:
    example: false
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.toolchains.plugins.get("example").unwrap(),
                &ProjectToolchainEntry::Enabled(false)
            );
        }

        #[test]
        fn can_enable_with_true() {
            let config = test_load_config(
                "moon.yml",
                r"
toolchains:
    example: true
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.toolchains.plugins.get("example").unwrap(),
                &ProjectToolchainEntry::Enabled(true)
            );
        }

        #[test]
        fn can_set_customg_config() {
            let config = test_load_config(
                "moon.yml",
                r"
toolchains:
    example:
        version: '1.2.3'
        custom: true
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(
                config.toolchains.plugins.get("example").unwrap(),
                &ProjectToolchainEntry::Config(ToolchainPluginConfig {
                    version: Some(UnresolvedVersionSpec::parse("1.2.3").unwrap()),
                    config: BTreeMap::from_iter([("custom".into(), serde_json::Value::Bool(true))]),
                    ..Default::default()
                })
            );
        }
    }

    mod workspace {
        use super::*;

        #[test]
        fn can_set_settings() {
            let config = test_load_config(
                "moon.yml",
                r"
workspace:
  inheritedTasks:
    exclude: [a]
    include: [b]
    rename:
      c: d
",
                |path| load_config_from_root(path, "."),
            );

            assert_eq!(config.workspace.inherited_tasks.exclude, vec![Id::raw("a")]);
            assert_eq!(
                config.workspace.inherited_tasks.include,
                Some(vec![Id::raw("b")])
            );
            assert_eq!(
                config.workspace.inherited_tasks.rename,
                FxHashMap::from_iter([(Id::raw("c"), Id::raw("d"))])
            );
        }
    }

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

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

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