use cfg::DnfExpr;
use stdx::format_to;

use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, Severity};

// Diagnostic: inactive-code
//
// This diagnostic is shown for code with inactive `#[cfg]` attributes.
//
// It can be disabled selectively with `#[allow(rust_analyzer::inactive_code)]`.
pub(crate) fn inactive_code(
    ctx: &DiagnosticsContext<'_, '_>,
    d: &hir::InactiveCode,
) -> Option<Diagnostic> {
    // If there's inactive code somewhere in a macro that doesn't map to something in the call, don't propagate to the call-site.
    d.node.map(|it| it.text_range()).original_node_file_range_rooted_opt(ctx.db())?;

    let inactive = DnfExpr::new(&d.cfg).why_inactive(&d.opts);
    let mut message = "code is inactive due to #[cfg] directives".to_owned();

    if let Some(inactive) = inactive {
        let inactive_reasons = inactive.to_string();

        if inactive_reasons.is_empty() {
            format_to!(message);
        } else {
            format_to!(message, ": {}", inactive);
        }
    }
    // FIXME: This shouldn't be a diagnostic
    let res = Diagnostic::new_with_syntax_node_ptr(
        ctx,
        DiagnosticCode::RaLint("inactive_code", Severity::WeakWarning),
        message,
        d.node,
    )
    .stable()
    .with_unused(true);
    Some(res)
}

#[cfg(test)]
mod tests {
    use ide_db::RootDatabase;
    use test_fixture::WithFixture;

    use crate::{DiagnosticCode, DiagnosticsConfig, tests::check_diagnostics_with_config};

    #[track_caller]
    pub(crate) fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
        let config = DiagnosticsConfig {
            disabled: std::iter::once("unlinked-file".to_owned()).collect(),
            ..DiagnosticsConfig::test_sample()
        };
        check_diagnostics_with_config(config, ra_fixture)
    }

    #[test]
    fn cfg_diagnostics() {
        check(
            r#"
fn f() {
    // The three g̶e̶n̶d̶e̶r̶s̶ statements:

    #[cfg(a)] fn f() {}  // Item statement
  //^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
    #[cfg(a)] {}         // Expression statement
  //^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
    #[cfg(a)] let x = 0; // let statement
  //^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled

    fn abc() {}
    abc(#[cfg(a)] 0);
      //^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
    let x = Struct {
        #[cfg(a)] f: 0,
      //^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
    };
    match () {
        () => (),
        #[cfg(a)] () => (),
      //^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
    }

    #[cfg(a)] 0          // Trailing expression of block
  //^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
}
        "#,
        );
    }

    #[test]
    fn inactive_item() {
        // Additional tests in `cfg` crate. This only tests disabled cfgs.

        check(
            r#"
    #[cfg(no)] pub fn f() {}
  //^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled

    #[cfg(no)] #[cfg(no2)] mod m;
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled

    #[cfg(all(not(a), b))] enum E {}
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: b is disabled

    #[cfg(feature = "std")] use std;
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: feature = "std" is disabled

    #[cfg(any())] pub fn f() {}
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives
"#,
        );
    }

    #[test]
    fn inactive_assoc_item() {
        check(
            r#"
struct Foo;
impl Foo {
    #[cfg(any())] pub fn f() {}
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives
}

trait Bar {
    #[cfg(any())] pub fn f() {}
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives
}
"#,
        );
    }

    /// Tests that `cfg` attributes behind `cfg_attr` is handled properly.
    #[test]
    fn inactive_via_cfg_attr() {
        check(
            r#"
    #[cfg_attr(not(never), cfg(no))] fn f() {}
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled

    #[cfg_attr(not(never), cfg(not(no)))] fn f() {}

    #[cfg_attr(never, cfg(no))] fn g() {}

    #[cfg_attr(not(never), inline, cfg(no))] fn h() {}
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled
"#,
        );
    }

    #[test]
    fn inactive_fields_and_variants() {
        check(
            r#"
enum Foo {
  #[cfg(a)] Bar,
//^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
  Baz {
    #[cfg(a)] baz: String,
  //^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
  },
  Qux(#[cfg(a)] String),
    //^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
}

struct Baz {
  #[cfg(a)] baz: String,
//^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
}

struct Qux(#[cfg(a)] String);
         //^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled

union FooBar {
  #[cfg(a)] baz: u32,
//^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
}
"#,
        );
    }

    #[test]
    fn modules() {
        check(
            r#"
//- /main.rs
  #[cfg(outline)] mod outline;
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: outline is disabled

  mod outline_inner;
//^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: outline_inner is disabled

  #[cfg(inline)] mod inline {}
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: inline is disabled

//- /outline_inner.rs
#![cfg(outline_inner)]
//- /outline.rs
"#,
        );
    }

    #[test]
    fn cfg_true_false() {
        check(
            r#"
  #[cfg(false)] fn inactive() {}
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: false is disabled

  #[cfg(true)] fn active() {}

  #[cfg(any(not(true), false))] fn inactive2() {}
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: true is enabled and false is disabled

"#,
        );
    }

    #[test]
    fn inactive_crate() {
        let db = RootDatabase::with_files(
            r#"
#![cfg(false)]

fn foo() {}
        "#,
        );
        let file_id = db.test_crate().root_file_id(&db);
        let diagnostics = hir::attach_db(&db, || {
            crate::full_diagnostics(
                &db,
                &DiagnosticsConfig::test_sample(),
                &ide_db::assists::AssistResolveStrategy::All,
                file_id.file_id(&db),
            )
        });
        let [inactive_code] = &*diagnostics else {
            panic!("expected one inactive_code diagnostic, found {diagnostics:#?}");
        };
        assert_eq!(
            inactive_code.code,
            DiagnosticCode::RaLint("inactive_code", ide_db::Severity::WeakWarning)
        );
        assert_eq!(
            inactive_code.message,
            "code is inactive due to #[cfg] directives: false is disabled",
        );
        assert!(inactive_code.fixes.is_none());
        let full_file_range = file_id.parse(&db).syntax_node().text_range();
        assert_eq!(
            inactive_code.range,
            ide_db::FileRange { file_id: file_id.file_id(&db), range: full_file_range },
        );
    }

    #[test]
    fn cfg_in_macro_does_not_diagnose_the_whole_call() {
        check(
            r#"
macro_rules! m {
    ($e:item) => {
        #[cfg(false)]
        const _: () = ();

        $e
    };
}

m! {
    fn foo() {}
}
        "#,
        );
    }

    #[test]
    fn in_macro() {
        check(
            r#"
macro_rules! m {
    ($e:item) => {
        $e
    };
}

m! {
    #[cfg(false)] fn foo() {}
 // ^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: false is disabled
}
        "#,
        );
        check(
            r#"
macro_rules! m {
    ($e:item) => {
        #[cfg(false)]
        $e
    };
}

m! {
    fn foo() {}
 // ^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: false is disabled
}
        "#,
        );
    }

    #[test]
    fn allow() {
        check(
            r#"
macro_rules! m {
    ($e:item) => {
        #[cfg(false)]
        #[allow(rust_analyzer::inactive_code)]
        $e
    };
}

m! {
    fn foo() {}
}
        "#,
        );
    }
}
