use super::super::inventory::KeyInfo;
use super::context::ValidationContext;
use super::*;
use crate::core::ValidationIssue;
use crate::ftl::LoadedFtlFile;
use es_fluent_shared::{
    fluent::{FluentArgumentName, FluentEntryId},
    resource::ModuleResourceSpec,
    source::{SourceFile, SourceLine},
};
use fs_err as fs;
use indexmap::IndexMap;
use std::path::PathBuf;

fn package(name: &str) -> es_fluent_runner::PackageName {
    es_fluent_runner::PackageName::try_new(name).expect("valid package name")
}

fn key_info(vars: &[&str], source_file: Option<&str>, source_line: Option<u32>) -> KeyInfo {
    key_info_with_resource(
        vars,
        source_file,
        source_line,
        ModuleResourceSpec::base("test-app", true),
    )
}

fn key_info_with_resource(
    vars: &[&str],
    source_file: Option<&str>,
    source_line: Option<u32>,
    resource: ModuleResourceSpec,
) -> KeyInfo {
    KeyInfo {
        variables: vars
            .iter()
            .map(|v| FluentArgumentName::try_new(*v).unwrap())
            .collect(),
        resource,
        source_file: source_file.and_then(SourceFile::new),
        source_line: source_line.map(SourceLine::new),
    }
}

#[test]
fn validate_loaded_ftl_files_reports_key_in_wrong_resource_as_missing_from_expected_route() {
    let temp = tempfile::tempdir().unwrap();
    let ftl_path = temp.path().join("i18n/en/test-app.ftl");
    fs::create_dir_all(ftl_path.parent().unwrap()).unwrap();
    fs::write(&ftl_path, "hello = Hello\n").unwrap();

    let resource = fluent_syntax::parser::parse("hello = Hello\n".to_string()).unwrap();
    let loaded_files = vec![LoadedFtlFile {
        abs_path: ftl_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource,
        keys: std::iter::once("hello".to_string()).collect(),
    }];

    let namespace = es_fluent_shared::namespace::ResolvedNamespace::new("ui").unwrap();
    let mut expected_keys = IndexMap::new();
    expected_keys.insert(
        expected_key("hello"),
        key_info_with_resource(
            &[],
            None,
            None,
            ModuleResourceSpec::namespaced("test-app", &namespace, true),
        ),
    );

    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };

    let issues = validate_loaded(&ctx, loaded_files, "en");
    assert!(issues.iter().any(|issue| {
        matches!(
            issue,
            ValidationIssue::MissingKey(err)
                if err.key == "hello"
                    && err.help.contains("en/test-app/ui.ftl")
                    && err.src.name().contains("test-app.ftl")
        )
    }));
}

fn expected_key(value: &str) -> FluentEntryId {
    FluentEntryId::try_new(value).unwrap()
}

fn validate_loaded(
    ctx: &ValidationContext<'_>,
    loaded_files: Vec<LoadedFtlFile>,
    locale: &str,
) -> Vec<ValidationIssue> {
    super::loaded::validate_loaded_ftl_files(ctx, loaded_files, locale, "en", None)
}

fn with_force_hyperlink<T>(value: &str, f: impl FnOnce() -> T) -> T {
    temp_env::with_var("FORCE_HYPERLINK", Some(value), f)
}

#[test]
fn missing_file_issues_returns_issue_for_each_expected_key() {
    let mut expected_keys = IndexMap::new();
    expected_keys.insert(expected_key("first"), key_info(&[], None, None));
    expected_keys.insert(expected_key("second"), key_info(&[], None, None));

    let temp = tempfile::tempdir().unwrap();
    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };

    let issues = ctx.missing_file_issues("en", "i18n/en/test-app.ftl");
    assert_eq!(issues.len(), 2);
    assert!(
        issues
            .iter()
            .any(|issue| matches!(issue, ValidationIssue::MissingKey(err) if err.key == "first"))
    );
    assert!(
        issues
            .iter()
            .any(|issue| matches!(issue, ValidationIssue::MissingKey(err) if err.key == "second"))
    );
}

#[test]
fn validate_loaded_ftl_files_reports_missing_key_and_variable() {
    let temp = tempfile::tempdir().unwrap();
    let ftl_path = temp.path().join("i18n/en/test-app.ftl");
    fs::create_dir_all(ftl_path.parent().unwrap()).unwrap();
    fs::write(&ftl_path, "hello = Hello\n").unwrap();

    let resource = fluent_syntax::parser::parse("hello = Hello\n".to_string()).unwrap();
    let loaded_files = vec![LoadedFtlFile {
        abs_path: ftl_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource,
        keys: std::iter::once("hello".to_string()).collect(),
    }];

    let mut expected_keys = IndexMap::new();
    expected_keys.insert(
        expected_key("hello"),
        key_info(&["name"], Some("src/lib.rs"), Some(7)),
    );
    expected_keys.insert(expected_key("goodbye"), key_info(&[], None, None));

    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };

    let issues = validate_loaded(&ctx, loaded_files, "en");
    assert!(issues.iter().any(|issue| {
        matches!(
            issue,
            ValidationIssue::MissingVariable(warning)
                if warning.key == "hello" && warning.variable == "name"
        )
    }));
    assert!(
        issues
            .iter()
            .any(|issue| matches!(issue, ValidationIssue::MissingKey(err) if err.key == "goodbye"))
    );
}

#[test]
fn validate_loaded_ftl_files_reports_unexpected_variable_as_error() {
    let temp = tempfile::tempdir().unwrap();
    let ftl_path = temp.path().join("i18n/en/test-app.ftl");
    fs::create_dir_all(ftl_path.parent().unwrap()).unwrap();
    fs::write(&ftl_path, "hello = Hello { $name } { $extra }\n").unwrap();

    let resource =
        fluent_syntax::parser::parse("hello = Hello { $name } { $extra }\n".to_string()).unwrap();
    let loaded_files = vec![LoadedFtlFile {
        abs_path: ftl_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource,
        keys: std::iter::once("hello".to_string()).collect(),
    }];

    let mut expected_keys = IndexMap::new();
    expected_keys.insert(expected_key("hello"), key_info(&["name"], None, None));

    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };

    let issues = validate_loaded(&ctx, loaded_files, "en");
    assert!(issues.iter().any(|issue| {
        matches!(
            issue,
            ValidationIssue::UnexpectedVariable(err)
                if err.key == "hello" && err.variable == "extra"
        )
    }));
}

#[test]
fn validate_loaded_ftl_files_reports_non_fallback_copy_as_untranslated() {
    let temp = tempfile::tempdir().unwrap();
    let fallback_path = temp.path().join("i18n/en/test-app.ftl");
    let target_path = temp.path().join("i18n/fr/test-app.ftl");
    fs::create_dir_all(fallback_path.parent().unwrap()).unwrap();
    fs::create_dir_all(target_path.parent().unwrap()).unwrap();
    fs::write(&fallback_path, "hello = Hello { $name }\n").unwrap();
    fs::write(&target_path, "hello = Hello { $name }\n").unwrap();

    let fallback_resource =
        fluent_syntax::parser::parse("hello = Hello { $name }\n".to_string()).unwrap();
    let target_resource =
        fluent_syntax::parser::parse("hello = Hello { $name }\n".to_string()).unwrap();
    let fallback_files = vec![LoadedFtlFile {
        abs_path: fallback_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource: fallback_resource,
        keys: std::iter::once("hello".to_string()).collect(),
    }];
    let target_files = vec![LoadedFtlFile {
        abs_path: target_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource: target_resource,
        keys: std::iter::once("hello".to_string()).collect(),
    }];

    let mut expected_keys = IndexMap::new();
    expected_keys.insert(expected_key("hello"), key_info(&["name"], None, None));

    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };
    let fallback_keys = super::loaded::collect_fallback_keys(&fallback_files);

    let issues = super::loaded::validate_loaded_ftl_files(
        &ctx,
        target_files,
        "fr",
        "en",
        Some(&fallback_keys),
    );
    assert!(issues.iter().any(|issue| {
        matches!(
            issue,
            ValidationIssue::UntranslatedMessage(warning)
                if warning.key == "hello"
                    && warning.locale == "fr"
                    && warning.fallback_locale == "en"
        )
    }));
}

#[test]
fn validate_loaded_ftl_files_allows_marked_same_as_fallback_message() {
    let temp = tempfile::tempdir().unwrap();
    let fallback_path = temp.path().join("i18n/en/test-app.ftl");
    let target_path = temp.path().join("i18n/fr/test-app.ftl");
    fs::create_dir_all(fallback_path.parent().unwrap()).unwrap();
    fs::create_dir_all(target_path.parent().unwrap()).unwrap();
    fs::write(&fallback_path, "brand = es-fluent\n").unwrap();
    fs::write(
        &target_path,
        "# es-fluent: same-as-fallback\nbrand = es-fluent\n",
    )
    .unwrap();

    let fallback_resource =
        fluent_syntax::parser::parse("brand = es-fluent\n".to_string()).unwrap();
    let target_resource = fluent_syntax::parser::parse(
        "# es-fluent: same-as-fallback\nbrand = es-fluent\n".to_string(),
    )
    .unwrap();
    let fallback_files = vec![LoadedFtlFile {
        abs_path: fallback_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource: fallback_resource,
        keys: std::iter::once("brand".to_string()).collect(),
    }];
    let target_files = vec![LoadedFtlFile {
        abs_path: target_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource: target_resource,
        keys: std::iter::once("brand".to_string()).collect(),
    }];

    let mut expected_keys = IndexMap::new();
    expected_keys.insert(expected_key("brand"), key_info(&[], None, None));

    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };
    let fallback_keys = super::loaded::collect_fallback_keys(&fallback_files);

    let issues = super::loaded::validate_loaded_ftl_files(
        &ctx,
        target_files,
        "fr",
        "en",
        Some(&fallback_keys),
    );
    assert!(
        !issues
            .iter()
            .any(|issue| matches!(issue, ValidationIssue::UntranslatedMessage(_)))
    );
}

#[test]
fn validate_loaded_ftl_files_allows_same_as_fallback_marker_before_next_message_entry() {
    let temp = tempfile::tempdir().unwrap();
    let fallback_path = temp.path().join("i18n/en/test-app.ftl");
    let target_path = temp.path().join("i18n/fr/test-app.ftl");
    fs::create_dir_all(fallback_path.parent().unwrap()).unwrap();
    fs::create_dir_all(target_path.parent().unwrap()).unwrap();
    fs::write(&fallback_path, "brand = es-fluent\n").unwrap();
    fs::write(
        &target_path,
        "# es-fluent: same-as-fallback\n\n# translator note\nbrand = es-fluent\n",
    )
    .unwrap();

    let fallback_resource =
        fluent_syntax::parser::parse("brand = es-fluent\n".to_string()).unwrap();
    let target_resource = fluent_syntax::parser::parse(
        "# es-fluent: same-as-fallback\n\n# translator note\nbrand = es-fluent\n".to_string(),
    )
    .unwrap();
    let fallback_files = vec![LoadedFtlFile {
        abs_path: fallback_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource: fallback_resource,
        keys: std::iter::once("brand".to_string()).collect(),
    }];
    let target_files = vec![LoadedFtlFile {
        abs_path: target_path,
        relative_path: PathBuf::from("test-app.ftl"),
        resource: target_resource,
        keys: std::iter::once("brand".to_string()).collect(),
    }];

    let mut expected_keys = IndexMap::new();
    expected_keys.insert(expected_key("brand"), key_info(&[], None, None));

    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };
    let fallback_keys = super::loaded::collect_fallback_keys(&fallback_files);

    let issues = super::loaded::validate_loaded_ftl_files(
        &ctx,
        target_files,
        "fr",
        "en",
        Some(&fallback_keys),
    );
    assert!(
        !issues
            .iter()
            .any(|issue| matches!(issue, ValidationIssue::UntranslatedMessage(_)))
    );
}

#[test]
fn validate_loaded_ftl_files_reports_duplicate_keys_and_ignores_non_messages() {
    let temp = tempfile::tempdir().unwrap();
    let first_path = temp.path().join("i18n/en/first.ftl");
    let duplicate_path = temp.path().join("i18n/en/duplicate.ftl");
    fs::create_dir_all(first_path.parent().unwrap()).unwrap();
    fs::write(&first_path, "hello = Hello\n-term = Term\n").unwrap();
    fs::write(&duplicate_path, "hello = Duplicate\n").unwrap();

    let first_resource =
        fluent_syntax::parser::parse("hello = Hello\n-term = Term\n".to_string()).unwrap();
    let duplicate_resource =
        fluent_syntax::parser::parse("hello = Duplicate\n".to_string()).unwrap();
    let loaded_files = vec![
        LoadedFtlFile {
            abs_path: first_path,
            relative_path: PathBuf::from("first.ftl"),
            resource: first_resource,
            keys: std::iter::once("hello".to_string()).collect(),
        },
        LoadedFtlFile {
            abs_path: duplicate_path,
            relative_path: PathBuf::from("duplicate.ftl"),
            resource: duplicate_resource,
            keys: std::iter::once("hello".to_string()).collect(),
        },
    ];

    let mut expected_keys = IndexMap::new();
    expected_keys.insert(expected_key("hello"), key_info(&[], None, None));
    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };

    let issues = validate_loaded(&ctx, loaded_files, "en");
    assert!(issues.iter().any(|issue| {
        matches!(
            issue,
            ValidationIssue::DuplicateKey(err)
                if err.key == "hello" && err.first_file.ends_with("first.ftl")
        )
    }));
}

#[test]
fn validate_crate_reports_missing_main_file_as_missing_key() {
    let temp = tempfile::tempdir().unwrap();
    fs::create_dir_all(temp.path().join("src")).unwrap();
    crate::test_fixtures::toml_helpers::write_toml(
        &temp.path().join("i18n.toml"),
        &crate::test_fixtures::toml_helpers::i18n_config("en", "i18n"),
    );

    let inventory_path = es_fluent_runner::RunnerMetadataStore::new(temp.path())
        .inventory_path(&package("test-crate"));
    fs::create_dir_all(inventory_path.parent().unwrap()).unwrap();
    fs::write(
        &inventory_path,
        r#"{
  "expected_keys": [
    {
      "key": "hello",
      "variables": [],
      "source_file": null,
      "source_line": null
    }
  ]
}"#,
    )
    .unwrap();

    let krate = CrateInfo {
        name: package("test-crate"),
        manifest_dir: crate::core::ManifestDir::from_discovered(temp.path().to_path_buf()),
        src_dir: crate::core::SourceDir::from_discovered(temp.path().join("src")),
        i18n_config_path: crate::core::DiscoveredI18nConfigPath::from_discovered(
            temp.path().join("i18n.toml"),
        ),
        ftl_output_dir: crate::core::DiscoveredFtlOutputDir::from_discovered(
            temp.path().join("i18n/en"),
        ),
        has_lib_rs: true,
        fluent_features: Vec::new(),
    };

    let issues = validate_crate(&krate, temp.path(), temp.path(), false, true).unwrap();
    assert_eq!(issues.len(), 1);
    assert!(
        issues
            .iter()
            .any(|issue| matches!(issue, ValidationIssue::MissingKey(err) if err.key == "hello"))
    );
}

#[test]
fn validate_crate_respects_config_disabled_fallback_copy_check() {
    let temp = tempfile::tempdir().unwrap();
    fs::create_dir_all(temp.path().join("src")).unwrap();
    fs::create_dir_all(temp.path().join("i18n/en")).unwrap();
    fs::create_dir_all(temp.path().join("i18n/fr")).unwrap();
    fs::write(
        temp.path().join("i18n.toml"),
        "fallback_language = \"en\"\nassets_dir = \"i18n\"\ncheck_fallback_copies = false\n",
    )
    .unwrap();
    fs::write(
        temp.path().join("i18n/en/test-crate.ftl"),
        "hello = Hello\n",
    )
    .unwrap();
    fs::write(
        temp.path().join("i18n/fr/test-crate.ftl"),
        "hello = Hello\n",
    )
    .unwrap();

    let inventory_path = es_fluent_runner::RunnerMetadataStore::new(temp.path())
        .inventory_path(&package("test-crate"));
    fs::create_dir_all(inventory_path.parent().unwrap()).unwrap();
    fs::write(
        &inventory_path,
        r#"{
  "expected_keys": [
    {
      "key": "hello",
      "variables": [],
      "source_file": null,
      "source_line": null
    }
  ]
}"#,
    )
    .unwrap();

    let krate = CrateInfo {
        name: package("test-crate"),
        manifest_dir: crate::core::ManifestDir::from_discovered(temp.path().to_path_buf()),
        src_dir: crate::core::SourceDir::from_discovered(temp.path().join("src")),
        i18n_config_path: crate::core::DiscoveredI18nConfigPath::from_discovered(
            temp.path().join("i18n.toml"),
        ),
        ftl_output_dir: crate::core::DiscoveredFtlOutputDir::from_discovered(
            temp.path().join("i18n/en"),
        ),
        has_lib_rs: true,
        fluent_features: Vec::new(),
    };

    let issues = validate_crate(&krate, temp.path(), temp.path(), true, true).unwrap();
    assert!(issues.is_empty());
}

#[test]
#[serial_test::serial(process)]
fn validate_loaded_ftl_files_handles_source_file_variants_and_terminal_links() {
    with_force_hyperlink("1", || {
        let temp = tempfile::tempdir().unwrap();
        let ftl_path = temp.path().join("i18n/en/test-app.ftl");
        fs::create_dir_all(ftl_path.parent().unwrap()).unwrap();
        fs::write(&ftl_path, "hello = Hello\nbye = Bye\nraw = Raw\n").unwrap();

        let resource =
            fluent_syntax::parser::parse("hello = Hello\nbye = Bye\nraw = Raw\n".to_string())
                .unwrap();
        let loaded_files = vec![LoadedFtlFile {
            abs_path: ftl_path,
            relative_path: PathBuf::from("test-app.ftl"),
            resource,
            keys: ["hello".to_string(), "bye".to_string(), "raw".to_string()]
                .into_iter()
                .collect(),
        }];

        let mut expected_keys = IndexMap::new();
        let absolute_source = temp
            .path()
            .join("src/lib.rs")
            .to_str()
            .expect("utf-8 test path")
            .to_string();
        expected_keys.insert(
            expected_key("hello"),
            key_info(&["name"], Some(&absolute_source), Some(7)),
        );
        expected_keys.insert(
            expected_key("bye"),
            key_info(&["who"], Some("src/lib.rs"), None),
        );
        expected_keys.insert(expected_key("raw"), key_info(&["value"], None, None));

        let ctx = ValidationContext {
            expected_keys: &expected_keys,
            workspace_root: temp.path(),
            manifest_dir: temp.path(),
        };

        let issues = validate_loaded(&ctx, loaded_files, "en");

        assert!(issues.iter().any(|issue| {
            matches!(
                issue,
                ValidationIssue::MissingVariable(warning)
                    if warning.key == "hello" && warning.help.contains("src/lib.rs:7")
            )
        }));
        assert!(issues.iter().any(|issue| {
            matches!(
                issue,
                ValidationIssue::MissingVariable(warning)
                    if warning.key == "bye" && warning.help.contains("declared in")
            )
        }));
        assert!(issues.iter().any(|issue| {
            matches!(
                issue,
                ValidationIssue::MissingVariable(warning)
                    if warning.key == "raw" && warning.help.contains("Rust code")
            )
        }));
    });
}

#[test]
fn validate_loaded_ftl_files_falls_back_to_unknown_when_no_actual_files() {
    let temp = tempfile::tempdir().unwrap();
    let mut expected_keys = IndexMap::new();
    expected_keys.insert(expected_key("missing"), key_info(&[], None, None));

    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: temp.path(),
        manifest_dir: temp.path(),
    };

    let issues = validate_loaded(&ctx, Vec::new(), "en");
    assert_eq!(issues.len(), 1);
    assert!(matches!(
        &issues[0],
        ValidationIssue::MissingKey(err) if err.key == "missing"
    ));
}

#[test]
fn validate_ftl_files_reports_syntax_issue_when_discovery_errors() {
    let temp = tempfile::tempdir().unwrap();
    let src_dir = temp.path().join("src");
    fs::create_dir_all(&src_dir).unwrap();
    fs::write(src_dir.join("lib.rs"), "pub struct Demo;\n").unwrap();
    crate::test_fixtures::toml_helpers::write_toml(
        &temp.path().join("i18n.toml"),
        &crate::test_fixtures::toml_helpers::i18n_config("en", "i18n"),
    );

    let broken_dir = temp.path().join("i18n/en/test-crate");
    fs::create_dir_all(broken_dir.parent().unwrap()).unwrap();
    fs::write(&broken_dir, "not a directory").unwrap();

    let krate = CrateInfo {
        name: package("test-crate"),
        manifest_dir: crate::core::ManifestDir::from_discovered(temp.path().to_path_buf()),
        src_dir: crate::core::SourceDir::from_discovered(src_dir),
        i18n_config_path: crate::core::DiscoveredI18nConfigPath::from_discovered(
            temp.path().join("i18n.toml"),
        ),
        ftl_output_dir: crate::core::DiscoveredFtlOutputDir::from_discovered(
            temp.path().join("i18n/en"),
        ),
        has_lib_rs: true,
        fluent_features: Vec::new(),
    };

    let issues = validate_ftl_files(&krate, temp.path(), &IndexMap::new(), false, true).unwrap();

    assert!(issues.iter().any(|issue| {
        matches!(issue, ValidationIssue::SyntaxError(err) if err.help.contains("Failed to discover FTL files"))
    }));
}

#[test]
fn to_relative_path_uses_non_canonical_strip_fallback() {
    let temp = tempfile::tempdir().unwrap();
    let real_root = temp.path().join("workspace-real");
    fs::create_dir_all(&real_root).unwrap();
    let alternate_parent = temp.path().join("alternate-parent");
    fs::create_dir_all(&alternate_parent).unwrap();
    let alternate_root = alternate_parent.join("..").join("workspace-real");

    let expected_keys = IndexMap::new();
    let ctx = ValidationContext {
        expected_keys: &expected_keys,
        workspace_root: &alternate_root,
        manifest_dir: &alternate_root,
    };

    let virtual_path = alternate_root.join("i18n/en/missing.ftl");
    let rel = ctx.to_relative_path(&virtual_path);
    assert_eq!(rel, "i18n/en/missing.ftl");

    let outside = temp.path().join("outside.ftl");
    let outside_rel = ctx.to_relative_path(&outside);
    assert_eq!(outside_rel, crate::utils::paths::slash_path(&outside));
}
