use serde_json::{Map, Value};

use super::knip_tables::{
    KNIP_PLUGIN_KEYS, KNIP_RULE_MAP, KNIP_UNMAPPABLE_FIELDS, KNIP_UNMAPPABLE_ISSUE_TYPES,
};
use super::{MigrationWarning, string_or_array};

type JsonMap = Map<String, Value>;

/// Migrate a string-or-array field from knip to a fallow config field.
pub(super) fn migrate_simple_field(
    obj: &JsonMap,
    src_key: &str,
    dst_key: &str,
    config: &mut JsonMap,
) {
    if let Some(val) = obj.get(src_key) {
        let entries = string_or_array(val);
        if !entries.is_empty() {
            config.insert(
                dst_key.to_string(),
                Value::Array(entries.into_iter().map(Value::String).collect()),
            );
        }
    }
}

/// Migrate knip `rules` to fallow `rules`, warning about unmappable rule names.
pub(super) fn migrate_rules(
    rules_val: &Value,
    config: &mut JsonMap,
    warnings: &mut Vec<MigrationWarning>,
) {
    let Some(rules_obj) = rules_val.as_object() else {
        return;
    };

    let mut fallow_rules = Map::new();
    for (knip_name, fallow_name) in KNIP_RULE_MAP {
        if let Some(severity_val) = rules_obj.get(*knip_name)
            && let Some(severity_str) = severity_val.as_str()
        {
            fallow_rules.insert(
                (*fallow_name).to_string(),
                Value::String(severity_str.to_string()),
            );
        }
    }

    // Warn about unmappable rule names
    for (key, _) in rules_obj {
        let is_mapped = KNIP_RULE_MAP.iter().any(|(k, _)| k == key);
        let is_unmappable = KNIP_UNMAPPABLE_ISSUE_TYPES.contains(&key.as_str());
        if !is_mapped && is_unmappable {
            warnings.push(MigrationWarning {
                source: "knip",
                field: format!("rules.{key}"),
                message: format!("issue type `{key}` has no fallow equivalent"),
                suggestion: None,
            });
        }
    }

    if !fallow_rules.is_empty() {
        config.insert("rules".to_string(), Value::Object(fallow_rules));
    }
}

/// Migrate knip `exclude` — set excluded issue types to `"off"` in fallow rules.
pub(super) fn migrate_exclude(
    excluded: &[String],
    config: &mut JsonMap,
    warnings: &mut Vec<MigrationWarning>,
) {
    let rules = config
        .entry("rules".to_string())
        .or_insert_with(|| Value::Object(Map::new()));
    let Some(rules_obj) = rules.as_object_mut() else {
        return;
    };

    for knip_name in excluded {
        if let Some((_, fallow_name)) = KNIP_RULE_MAP.iter().find(|(k, _)| k == knip_name) {
            rules_obj.insert((*fallow_name).to_string(), Value::String("off".to_string()));
        } else if KNIP_UNMAPPABLE_ISSUE_TYPES.contains(&knip_name.as_str()) {
            warnings.push(MigrationWarning {
                source: "knip",
                field: format!("exclude.{knip_name}"),
                message: format!("issue type `{knip_name}` has no fallow equivalent"),
                suggestion: None,
            });
        }
    }
}

/// Migrate knip `include` — set non-included issue types to `"off"` in fallow rules.
pub(super) fn migrate_include(
    included: &[String],
    config: &mut JsonMap,
    warnings: &mut Vec<MigrationWarning>,
) {
    let rules = config
        .entry("rules".to_string())
        .or_insert_with(|| Value::Object(Map::new()));
    let Some(rules_obj) = rules.as_object_mut() else {
        return;
    };

    for (knip_name, fallow_name) in KNIP_RULE_MAP {
        if !included.iter().any(|i| i == knip_name) {
            // Not included -- set to off (unless already set by rules)
            rules_obj
                .entry((*fallow_name).to_string())
                .or_insert_with(|| Value::String("off".to_string()));
        }
    }
    // Warn about unmappable included types
    for name in included {
        let is_mapped = KNIP_RULE_MAP.iter().any(|(k, _)| k == name);
        if !is_mapped && KNIP_UNMAPPABLE_ISSUE_TYPES.contains(&name.as_str()) {
            warnings.push(MigrationWarning {
                source: "knip",
                field: format!("include.{name}"),
                message: format!("issue type `{name}` has no fallow equivalent"),
                suggestion: None,
            });
        }
    }
}

/// Migrate knip `ignoreDependencies` — filter out regex patterns with warnings.
pub(super) fn migrate_ignore_deps(
    ignore_deps_val: &Value,
    config: &mut JsonMap,
    warnings: &mut Vec<MigrationWarning>,
) {
    let deps = string_or_array(ignore_deps_val);
    let non_regex: Vec<String> = deps
        .into_iter()
        .filter(|d| {
            // Skip values that look like regex patterns
            if d.starts_with('/') && d.ends_with('/') {
                warnings.push(MigrationWarning {
                    source: "knip",
                    field: "ignoreDependencies".to_string(),
                    message: format!("regex pattern `{d}` skipped (fallow uses exact strings)"),
                    suggestion: Some("add each dependency name explicitly".to_string()),
                });
                false
            } else {
                true
            }
        })
        .collect();
    if !non_regex.is_empty() {
        config.insert(
            "ignoreDependencies".to_string(),
            Value::Array(non_regex.into_iter().map(Value::String).collect()),
        );
    }
}

/// Warn about knip fields that have no fallow equivalent.
pub(super) fn warn_unmappable_fields(obj: &JsonMap, warnings: &mut Vec<MigrationWarning>) {
    for (field, message, suggestion) in KNIP_UNMAPPABLE_FIELDS {
        if obj.contains_key(*field) {
            warnings.push(MigrationWarning {
                source: "knip",
                field: (*field).to_string(),
                message: (*message).to_string(),
                suggestion: suggestion.map(std::string::ToString::to_string),
            });
        }
    }
}

/// Warn about knip plugin-specific config keys that are auto-detected in fallow.
pub(super) fn warn_plugin_keys(obj: &JsonMap, warnings: &mut Vec<MigrationWarning>) {
    for key in obj.keys() {
        if KNIP_PLUGIN_KEYS.contains(&key.as_str()) {
            warnings.push(MigrationWarning {
                source: "knip",
                field: key.clone(),
                message: format!(
                    "plugin config `{key}` is auto-detected by fallow's built-in plugins"
                ),
                suggestion: Some(
                    "remove this section; fallow detects framework config automatically"
                        .to_string(),
                ),
            });
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn empty_config() -> JsonMap {
        Map::new()
    }

    // -- migrate_simple_field -------------------------------------------------

    #[test]
    fn simple_field_present_array() {
        let obj: JsonMap =
            serde_json::from_str(r#"{"entry": ["src/index.ts", "src/main.ts"]}"#).unwrap();
        let mut config = empty_config();
        migrate_simple_field(&obj, "entry", "entry", &mut config);

        assert_eq!(
            config.get("entry").unwrap(),
            &json!(["src/index.ts", "src/main.ts"])
        );
    }

    #[test]
    fn simple_field_present_string() {
        let obj: JsonMap = serde_json::from_str(r#"{"entry": "src/index.ts"}"#).unwrap();
        let mut config = empty_config();
        migrate_simple_field(&obj, "entry", "entry", &mut config);

        assert_eq!(config.get("entry").unwrap(), &json!(["src/index.ts"]));
    }

    #[test]
    fn simple_field_absent() {
        let obj: JsonMap = serde_json::from_str(r#"{"other": "value"}"#).unwrap();
        let mut config = empty_config();
        migrate_simple_field(&obj, "entry", "entry", &mut config);

        assert!(!config.contains_key("entry"));
    }

    #[test]
    fn simple_field_renames_key() {
        let obj: JsonMap = serde_json::from_str(r#"{"ignore": ["**/*.test.ts"]}"#).unwrap();
        let mut config = empty_config();
        migrate_simple_field(&obj, "ignore", "ignorePatterns", &mut config);

        assert!(!config.contains_key("ignore"));
        assert_eq!(
            config.get("ignorePatterns").unwrap(),
            &json!(["**/*.test.ts"])
        );
    }

    #[test]
    fn simple_field_non_string_non_array_skipped() {
        let obj: JsonMap = serde_json::from_str(r#"{"entry": 42}"#).unwrap();
        let mut config = empty_config();
        migrate_simple_field(&obj, "entry", "entry", &mut config);

        assert!(!config.contains_key("entry"));
    }

    #[test]
    fn simple_field_empty_array_skipped() {
        let obj: JsonMap = serde_json::from_str(r#"{"entry": []}"#).unwrap();
        let mut config = empty_config();
        migrate_simple_field(&obj, "entry", "entry", &mut config);

        assert!(!config.contains_key("entry"));
    }

    // -- migrate_rules --------------------------------------------------------

    #[test]
    fn rules_known_mapping() {
        let rules_val = json!({"files": "error", "exports": "warn"});
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_rules(&rules_val, &mut config, &mut warnings);

        let rules = config.get("rules").unwrap().as_object().unwrap();
        assert_eq!(rules.get("unused-files").unwrap(), "error");
        assert_eq!(rules.get("unused-exports").unwrap(), "warn");
        assert!(warnings.is_empty());
    }

    #[test]
    fn rules_unknown_unmappable_generates_warning() {
        let rules_val = json!({"binaries": "warn"});
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_rules(&rules_val, &mut config, &mut warnings);

        assert!(!config.contains_key("rules"));
        assert_eq!(warnings.len(), 1);
        assert_eq!(warnings[0].field, "rules.binaries");
        assert!(warnings[0].message.contains("no fallow equivalent"));
    }

    #[test]
    fn rules_unknown_not_in_unmappable_silently_ignored() {
        let rules_val = json!({"totallyUnknown": "error"});
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_rules(&rules_val, &mut config, &mut warnings);

        assert!(!config.contains_key("rules"));
        assert!(warnings.is_empty());
    }

    #[test]
    fn rules_empty_object() {
        let rules_val = json!({});
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_rules(&rules_val, &mut config, &mut warnings);

        assert!(!config.contains_key("rules"));
        assert!(warnings.is_empty());
    }

    #[test]
    fn rules_non_object_is_noop() {
        let rules_val = json!("not-an-object");
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_rules(&rules_val, &mut config, &mut warnings);

        assert!(!config.contains_key("rules"));
        assert!(warnings.is_empty());
    }

    // -- migrate_exclude ------------------------------------------------------

    #[test]
    fn exclude_single_known_type() {
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_exclude(&["files".to_string()], &mut config, &mut warnings);

        let rules = config.get("rules").unwrap().as_object().unwrap();
        assert_eq!(rules.get("unused-files").unwrap(), "off");
        assert!(warnings.is_empty());
    }

    #[test]
    fn exclude_multiple_types() {
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_exclude(
            &[
                "files".to_string(),
                "types".to_string(),
                "duplicates".to_string(),
            ],
            &mut config,
            &mut warnings,
        );

        let rules = config.get("rules").unwrap().as_object().unwrap();
        assert_eq!(rules.get("unused-files").unwrap(), "off");
        assert_eq!(rules.get("unused-types").unwrap(), "off");
        assert_eq!(rules.get("duplicate-exports").unwrap(), "off");
        assert!(warnings.is_empty());
    }

    #[test]
    fn exclude_unmappable_type_warns() {
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_exclude(
            &["optionalPeerDependencies".to_string()],
            &mut config,
            &mut warnings,
        );

        assert_eq!(warnings.len(), 1);
        assert!(warnings[0].field.contains("optionalPeerDependencies"));
    }

    #[test]
    fn exclude_empty_slice() {
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_exclude(&[], &mut config, &mut warnings);

        // Empty rules object is still created via or_insert_with, but has no entries
        let rules = config.get("rules").unwrap().as_object().unwrap();
        assert!(rules.is_empty());
        assert!(warnings.is_empty());
    }

    // -- migrate_include ------------------------------------------------------

    #[test]
    fn include_known_types_sets_others_to_off() {
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_include(
            &["files".to_string(), "exports".to_string()],
            &mut config,
            &mut warnings,
        );

        let rules = config.get("rules").unwrap().as_object().unwrap();
        // Included types should NOT be in rules
        assert!(!rules.contains_key("unused-files"));
        assert!(!rules.contains_key("unused-exports"));
        // Non-included should be "off"
        assert_eq!(rules.get("unused-dependencies").unwrap(), "off");
        assert_eq!(rules.get("unused-types").unwrap(), "off");
        assert!(warnings.is_empty());
    }

    #[test]
    fn include_unmappable_type_warns() {
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_include(
            &["files".to_string(), "binaries".to_string()],
            &mut config,
            &mut warnings,
        );

        assert_eq!(warnings.len(), 1);
        assert_eq!(warnings[0].field, "include.binaries");
    }

    #[test]
    fn include_respects_existing_rules() {
        let mut config = empty_config();
        // Pre-set a rule
        let mut rules = Map::new();
        rules.insert(
            "unused-dependencies".to_string(),
            Value::String("warn".to_string()),
        );
        config.insert("rules".to_string(), Value::Object(rules));

        let mut warnings = Vec::new();
        migrate_include(&["files".to_string()], &mut config, &mut warnings);

        let rules = config.get("rules").unwrap().as_object().unwrap();
        // "unused-dependencies" was already "warn", include should not override to "off"
        assert_eq!(rules.get("unused-dependencies").unwrap(), "warn");
    }

    // -- migrate_ignore_deps --------------------------------------------------

    #[test]
    fn ignore_deps_plain_strings() {
        let val = json!(["lodash", "react"]);
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_ignore_deps(&val, &mut config, &mut warnings);

        assert_eq!(
            config.get("ignoreDependencies").unwrap(),
            &json!(["lodash", "react"])
        );
        assert!(warnings.is_empty());
    }

    #[test]
    fn ignore_deps_regex_filtered_with_warning() {
        let val = json!(["/^@scope/", "lodash"]);
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_ignore_deps(&val, &mut config, &mut warnings);

        assert_eq!(
            config.get("ignoreDependencies").unwrap(),
            &json!(["lodash"])
        );
        assert_eq!(warnings.len(), 1);
        assert!(warnings[0].message.contains("regex pattern"));
    }

    #[test]
    fn ignore_deps_all_regex_no_config_key() {
        let val = json!(["/^@a/", "/^@b/"]);
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_ignore_deps(&val, &mut config, &mut warnings);

        assert!(!config.contains_key("ignoreDependencies"));
        assert_eq!(warnings.len(), 2);
    }

    #[test]
    fn ignore_deps_single_string() {
        let val = json!("lodash");
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_ignore_deps(&val, &mut config, &mut warnings);

        assert_eq!(
            config.get("ignoreDependencies").unwrap(),
            &json!(["lodash"])
        );
    }

    #[test]
    fn ignore_deps_non_string_value_skipped() {
        let val = json!(42);
        let mut config = empty_config();
        let mut warnings = Vec::new();
        migrate_ignore_deps(&val, &mut config, &mut warnings);

        assert!(!config.contains_key("ignoreDependencies"));
        assert!(warnings.is_empty());
    }

    // -- warn_unmappable_fields -----------------------------------------------

    #[test]
    fn warn_unmappable_fields_detects_known_fields() {
        let obj: JsonMap =
            serde_json::from_str(r#"{"project": ["src/**"], "compilers": {}}"#).unwrap();
        let mut warnings = Vec::new();
        warn_unmappable_fields(&obj, &mut warnings);

        assert_eq!(warnings.len(), 2);
        let fields: Vec<&str> = warnings.iter().map(|w| w.field.as_str()).collect();
        assert!(fields.contains(&"project"));
        assert!(fields.contains(&"compilers"));
    }

    #[test]
    fn warn_unmappable_fields_empty_object_no_warnings() {
        let obj = Map::new();
        let mut warnings = Vec::new();
        warn_unmappable_fields(&obj, &mut warnings);

        assert!(warnings.is_empty());
    }

    #[test]
    fn warn_unmappable_fields_unrelated_keys_no_warnings() {
        let obj: JsonMap = serde_json::from_str(r#"{"entry": ["x"], "rules": {}}"#).unwrap();
        let mut warnings = Vec::new();
        warn_unmappable_fields(&obj, &mut warnings);

        assert!(warnings.is_empty());
    }

    #[test]
    fn warn_unmappable_fields_suggestion_presence() {
        // "ignoreFiles" has a suggestion, "project" does not
        let obj: JsonMap =
            serde_json::from_str(r#"{"ignoreFiles": ["x.ts"], "project": ["src"]}"#).unwrap();
        let mut warnings = Vec::new();
        warn_unmappable_fields(&obj, &mut warnings);

        let ignore_files_warning = warnings.iter().find(|w| w.field == "ignoreFiles").unwrap();
        assert!(ignore_files_warning.suggestion.is_some());

        let project_warning = warnings.iter().find(|w| w.field == "project").unwrap();
        assert!(project_warning.suggestion.is_none());
    }

    // -- warn_plugin_keys -----------------------------------------------------

    #[test]
    fn warn_plugin_keys_detects_plugins() {
        let obj: JsonMap =
            serde_json::from_str(r#"{"eslint": {"entry": ["a.js"]}, "jest": true}"#).unwrap();
        let mut warnings = Vec::new();
        warn_plugin_keys(&obj, &mut warnings);

        assert_eq!(warnings.len(), 2);
        let fields: Vec<&str> = warnings.iter().map(|w| w.field.as_str()).collect();
        assert!(fields.contains(&"eslint"));
        assert!(fields.contains(&"jest"));
        // All plugin warnings should have suggestions
        for w in &warnings {
            assert!(w.suggestion.is_some());
        }
    }

    #[test]
    fn warn_plugin_keys_empty_object_no_warnings() {
        let obj = Map::new();
        let mut warnings = Vec::new();
        warn_plugin_keys(&obj, &mut warnings);

        assert!(warnings.is_empty());
    }

    #[test]
    fn warn_plugin_keys_non_plugin_keys_no_warnings() {
        let obj: JsonMap =
            serde_json::from_str(r#"{"entry": ["x"], "ignore": ["y"], "rules": {}}"#).unwrap();
        let mut warnings = Vec::new();
        warn_plugin_keys(&obj, &mut warnings);

        assert!(warnings.is_empty());
    }
}
