#![allow(clippy::print_stderr)]

use ide_db::{
    assists::AssistResolveStrategy, base_db::SourceDatabase, LineIndexDatabase, RootDatabase,
};
use itertools::Itertools;
use stdx::trim_indent;
use test_fixture::WithFixture;
use test_utils::{assert_eq_text, extract_annotations, MiniCore};

use crate::{DiagnosticsConfig, ExprFillDefaultMode, Severity};

/// Takes a multi-file input fixture with annotated cursor positions,
/// and checks that:
///  * a diagnostic is produced
///  * the first diagnostic fix trigger range touches the input cursor position
///  * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
#[track_caller]
pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
    check_nth_fix(0, ra_fixture_before, ra_fixture_after);
}
/// Takes a multi-file input fixture with annotated cursor positions,
/// and checks that:
///  * a diagnostic is produced
///  * every diagnostic fixes trigger range touches the input cursor position
///  * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
    for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
        check_nth_fix(i, ra_fixture_before, ra_fixture_after)
    }
}

#[track_caller]
fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
    let mut config = DiagnosticsConfig::test_sample();
    config.expr_fill_default = ExprFillDefaultMode::Default;
    check_nth_fix_with_config(config, nth, ra_fixture_before, ra_fixture_after)
}

#[track_caller]
pub(crate) fn check_fix_with_disabled(
    ra_fixture_before: &str,
    ra_fixture_after: &str,
    disabled: impl Iterator<Item = String>,
) {
    let mut config = DiagnosticsConfig::test_sample();
    config.expr_fill_default = ExprFillDefaultMode::Default;
    config.disabled.extend(disabled);
    check_nth_fix_with_config(config, 0, ra_fixture_before, ra_fixture_after)
}

#[track_caller]
fn check_nth_fix_with_config(
    config: DiagnosticsConfig,
    nth: usize,
    ra_fixture_before: &str,
    ra_fixture_after: &str,
) {
    let after = trim_indent(ra_fixture_after);

    let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
    let diagnostic = super::full_diagnostics(
        &db,
        &config,
        &AssistResolveStrategy::All,
        file_position.file_id.into(),
    )
    .pop()
    .expect("no diagnostics");
    let fix = &diagnostic
        .fixes
        .unwrap_or_else(|| panic!("{:?} diagnostic misses fixes", diagnostic.code))[nth];
    let actual = {
        let source_change = fix.source_change.as_ref().unwrap();
        let file_id = *source_change.source_file_edits.keys().next().unwrap();
        let mut actual = db.file_text(file_id).to_string();

        for (edit, snippet_edit) in source_change.source_file_edits.values() {
            edit.apply(&mut actual);
            if let Some(snippet_edit) = snippet_edit {
                snippet_edit.apply(&mut actual);
            }
        }
        actual
    };

    assert!(
        fix.target.contains_inclusive(file_position.offset),
        "diagnostic fix range {:?} does not touch cursor position {:?}",
        fix.target,
        file_position.offset
    );
    assert_eq_text!(&after, &actual);
}

pub(crate) fn check_fixes_unordered(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
    for ra_fixture_after in ra_fixtures_after.iter() {
        check_has_fix(ra_fixture_before, ra_fixture_after)
    }
}

#[track_caller]
pub(crate) fn check_has_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
    let after = trim_indent(ra_fixture_after);

    let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
    let mut conf = DiagnosticsConfig::test_sample();
    conf.expr_fill_default = ExprFillDefaultMode::Default;
    let fix = super::full_diagnostics(
        &db,
        &conf,
        &AssistResolveStrategy::All,
        file_position.file_id.into(),
    )
    .into_iter()
    .find(|d| {
        d.fixes
            .as_ref()
            .and_then(|fixes| {
                fixes.iter().find(|fix| {
                    if !fix.target.contains_inclusive(file_position.offset) {
                        return false;
                    }
                    let actual = {
                        let source_change = fix.source_change.as_ref().unwrap();
                        let file_id = *source_change.source_file_edits.keys().next().unwrap();
                        let mut actual = db.file_text(file_id).to_string();

                        for (edit, snippet_edit) in source_change.source_file_edits.values() {
                            edit.apply(&mut actual);
                            if let Some(snippet_edit) = snippet_edit {
                                snippet_edit.apply(&mut actual);
                            }
                        }
                        actual
                    };
                    after == actual
                })
            })
            .is_some()
    });
    assert!(fix.is_some(), "no diagnostic with desired fix");
}

#[track_caller]
pub(crate) fn check_has_single_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
    let after = trim_indent(ra_fixture_after);

    let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
    let mut conf = DiagnosticsConfig::test_sample();
    conf.expr_fill_default = ExprFillDefaultMode::Default;
    let mut n_fixes = 0;
    let fix = super::full_diagnostics(
        &db,
        &conf,
        &AssistResolveStrategy::All,
        file_position.file_id.into(),
    )
    .into_iter()
    .find(|d| {
        d.fixes
            .as_ref()
            .and_then(|fixes| {
                n_fixes += fixes.len();
                fixes.iter().find(|fix| {
                    if !fix.target.contains_inclusive(file_position.offset) {
                        return false;
                    }
                    let actual = {
                        let source_change = fix.source_change.as_ref().unwrap();
                        let file_id = *source_change.source_file_edits.keys().next().unwrap();
                        let mut actual = db.file_text(file_id).to_string();

                        for (edit, snippet_edit) in source_change.source_file_edits.values() {
                            edit.apply(&mut actual);
                            if let Some(snippet_edit) = snippet_edit {
                                snippet_edit.apply(&mut actual);
                            }
                        }
                        actual
                    };
                    after == actual
                })
            })
            .is_some()
    });
    assert!(fix.is_some(), "no diagnostic with desired fix");
    assert!(n_fixes == 1, "Too many fixes suggested");
}

/// Checks that there's a diagnostic *without* fix at `$0`.
pub(crate) fn check_no_fix(ra_fixture: &str) {
    let (db, file_position) = RootDatabase::with_position(ra_fixture);
    let diagnostic = super::full_diagnostics(
        &db,
        &DiagnosticsConfig::test_sample(),
        &AssistResolveStrategy::All,
        file_position.file_id.into(),
    )
    .pop()
    .unwrap();
    assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {diagnostic:?}");
}

#[track_caller]
pub(crate) fn check_diagnostics(ra_fixture: &str) {
    let mut config = DiagnosticsConfig::test_sample();
    config.disabled.insert("inactive-code".to_owned());
    check_diagnostics_with_config(config, ra_fixture)
}

#[track_caller]
pub(crate) fn check_diagnostics_with_disabled(ra_fixture: &str, disabled: &[&str]) {
    let mut config = DiagnosticsConfig::test_sample();
    config.disabled.extend(disabled.iter().map(|&s| s.to_owned()));
    check_diagnostics_with_config(config, ra_fixture)
}

#[track_caller]
pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
    let (db, files) = RootDatabase::with_many_files(ra_fixture);
    let mut annotations = files
        .iter()
        .copied()
        .flat_map(|file_id| {
            super::full_diagnostics(&db, &config, &AssistResolveStrategy::All, file_id.into())
                .into_iter()
                .map(|d| {
                    let mut annotation = String::new();
                    if let Some(fixes) = &d.fixes {
                        assert!(!fixes.is_empty());
                        annotation.push_str("💡 ")
                    }
                    annotation.push_str(match d.severity {
                        Severity::Error => "error",
                        Severity::WeakWarning => "weak",
                        Severity::Warning => "warn",
                        Severity::Allow => "allow",
                    });
                    annotation.push_str(": ");
                    annotation.push_str(&d.message);
                    (d.range, annotation)
                })
        })
        .map(|(diagnostic, annotation)| (diagnostic.file_id, (diagnostic.range, annotation)))
        .into_group_map();
    for file_id in files {
        let file_id = file_id.into();
        let line_index = db.line_index(file_id);

        let mut actual = annotations.remove(&file_id).unwrap_or_default();
        let expected = extract_annotations(&db.file_text(file_id));
        actual.sort_by_key(|(range, _)| range.start());
        // FIXME: We should panic on duplicates instead, but includes currently cause us to report
        // diagnostics twice for the calling module when both files are queried.
        actual.dedup();
        // actual.iter().duplicates().for_each(|(range, msg)| {
        //     panic!("duplicate diagnostic at {:?}: {msg:?}", line_index.line_col(range.start()))
        // });
        if expected.is_empty() {
            // makes minicore smoke test debuggable
            for (e, _) in &actual {
                eprintln!(
                    "Code in range {e:?} = {}",
                    &db.file_text(file_id)[usize::from(e.start())..usize::from(e.end())]
                )
            }
        }
        if expected != actual {
            let fneg = expected
                .iter()
                .filter(|x| !actual.contains(x))
                .map(|(range, s)| (line_index.line_col(range.start()), range, s))
                .collect::<Vec<_>>();
            let fpos = actual
                .iter()
                .filter(|x| !expected.contains(x))
                .map(|(range, s)| (line_index.line_col(range.start()), range, s))
                .collect::<Vec<_>>();

            panic!("Diagnostic test failed.\nFalse negatives: {fneg:?}\nFalse positives: {fpos:?}");
        }
    }
}

#[test]
fn test_disabled_diagnostics() {
    let mut config = DiagnosticsConfig::test_sample();
    config.disabled.insert("E0583".into());

    let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#);
    let file_id = file_id.into();

    let diagnostics = super::full_diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
    assert!(diagnostics.is_empty());

    let diagnostics = super::full_diagnostics(
        &db,
        &DiagnosticsConfig::test_sample(),
        &AssistResolveStrategy::All,
        file_id,
    );
    assert!(!diagnostics.is_empty());
}

#[test]
fn minicore_smoke_test() {
    if test_utils::skip_slow_tests() {
        return;
    }

    fn check(minicore: MiniCore) {
        let source = minicore.source_code();
        let mut config = DiagnosticsConfig::test_sample();
        // This should be ignored since we conditionally remove code which creates single item use with braces
        config.disabled.insert("unused_braces".to_owned());
        config.disabled.insert("unused_variables".to_owned());
        config.disabled.insert("remove-unnecessary-else".to_owned());
        check_diagnostics_with_config(config, &source);
    }

    // Checks that there is no diagnostic in minicore for each flag.
    for flag in MiniCore::available_flags() {
        if flag == "clone" {
            // Clone without copy has `moved-out-of-ref`, so ignoring.
            // FIXME: Maybe we should merge copy and clone in a single flag?
            continue;
        }
        eprintln!("Checking minicore flag {flag}");
        check(MiniCore::from_flags([flag]));
    }
    // And one time for all flags, to check codes which are behind multiple flags + prevent name collisions
    eprintln!("Checking all minicore flags");
    check(MiniCore::from_flags(MiniCore::available_flags()))
}
