use crate::{Checker, Diagnostic, Edit, Fix, FixAvailability, Rule, ShellDialect, Violation};

pub struct SetFlagsWithoutDashes;

impl Violation for SetFlagsWithoutDashes {
    const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;

    fn rule() -> Rule {
        Rule::SetFlagsWithoutDashes
    }

    fn message(&self) -> String {
        "flags passed to `set` should start with `-` or `+`".to_owned()
    }

    fn fix_title(&self) -> Option<String> {
        Some("insert `--` before positional arguments".to_owned())
    }
}

pub fn set_flags_without_dashes(checker: &mut Checker) {
    if !matches!(
        checker.shell(),
        ShellDialect::Unknown
            | ShellDialect::Sh
            | ShellDialect::Bash
            | ShellDialect::Dash
            | ShellDialect::Ksh
    ) {
        return;
    }

    let spans = checker
        .facts()
        .commands()
        .iter()
        .filter(|fact| fact.effective_name_is("set"))
        .filter_map(|fact| fact.options().set())
        .flat_map(|set| set.flags_without_prefix_spans().iter().copied())
        .collect::<Vec<_>>();

    for span in spans {
        checker.report_diagnostic_dedup(
            Diagnostic::new(SetFlagsWithoutDashes, span)
                .with_fix(Fix::safe_edit(Edit::insertion(span.start.offset(), "-- "))),
        );
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use crate::test::{test_path_with_fix, test_snippet, test_snippet_with_fix};
    use crate::{Applicability, LinterSettings, Rule, ShellDialect, assert_diagnostics_diff};

    #[test]
    fn reports_set_flags_without_prefix() {
        let source = "\
set euox pipefail
set foo bar
";
        let diagnostics = test_snippet(
            source,
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes).with_shell(ShellDialect::Bash),
        );

        assert_eq!(diagnostics.len(), 2);
        assert_eq!(
            diagnostics
                .iter()
                .map(|diagnostic| diagnostic.span.slice(source))
                .collect::<Vec<_>>(),
            vec!["euox", "foo"]
        );
    }

    #[test]
    fn ignores_explicit_prefixes_or_single_positional_values() {
        let source = "\
set -euo pipefail
set +e
set -- foo bar
set foo
set n-aliases.conf n-env.conf
";
        let diagnostics = test_snippet(
            source,
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes).with_shell(ShellDialect::Bash),
        );

        assert!(diagnostics.is_empty());
    }

    #[test]
    fn ignores_quoted_literals_but_keeps_unknown_shells_enabled() {
        let source = "\
set \"required\" \"$1\"
set f\"oo\" bar
set OFFLINE_PATH \"$PWD\"
";
        let bash_diagnostics = test_snippet(
            source,
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes).with_shell(ShellDialect::Bash),
        );
        assert_eq!(
            bash_diagnostics
                .iter()
                .map(|diagnostic| diagnostic.span.slice(source))
                .collect::<Vec<_>>(),
            vec!["OFFLINE_PATH"]
        );

        let unknown_source = "set foo bar\n";
        let unknown_shell_diagnostics = test_snippet(
            unknown_source,
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes)
                .with_shell(ShellDialect::Unknown),
        );
        assert_eq!(
            unknown_shell_diagnostics
                .iter()
                .map(|diagnostic| diagnostic.span.slice(unknown_source))
                .collect::<Vec<_>>(),
            vec!["foo"]
        );
    }

    #[test]
    fn ignores_unsupported_known_shells() {
        let source = "set foo bar\n";
        let diagnostics = test_snippet(
            source,
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes).with_shell(ShellDialect::Zsh),
        );

        assert!(diagnostics.is_empty());
    }

    #[test]
    fn applies_safe_fix_to_bare_set_operands() {
        let source = "\
set euox pipefail
set foo bar
";
        let result = test_snippet_with_fix(
            source,
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes).with_shell(ShellDialect::Bash),
            Applicability::Safe,
        );

        assert_eq!(result.fixes_applied, 2);
        assert_eq!(
            result.fixed_source,
            "\
set -- euox pipefail
set -- foo bar
"
        );
        assert!(result.fixed_diagnostics.is_empty());
    }

    #[test]
    fn leaves_explicit_and_unambiguous_set_operands_unchanged_when_fixing() {
        let source = "\
set -euo pipefail
set -- foo bar
set foo
";
        let result = test_snippet_with_fix(
            source,
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes).with_shell(ShellDialect::Bash),
            Applicability::Safe,
        );

        assert_eq!(result.fixes_applied, 0);
        assert_eq!(result.fixed_source, source);
        assert!(result.fixed_diagnostics.is_empty());
    }

    #[test]
    fn snapshots_safe_fix_output_for_fixture() -> anyhow::Result<()> {
        let result = test_path_with_fix(
            Path::new("correctness").join("C098.sh").as_path(),
            &LinterSettings::for_rule(Rule::SetFlagsWithoutDashes).with_shell(ShellDialect::Bash),
            Applicability::Safe,
        )?;

        assert_diagnostics_diff!("C098_fix_C098.sh", result);
        Ok(())
    }
}
