#![feature(rustc_private)]
#![warn(unused_extern_crates)]

extern crate rustc_hir;
extern crate rustc_middle;
extern crate rustc_span;

use clippy_utils::diagnostics::span_lint_and_then;
use rustc_hir::{Expr, ExprKind, GenericArg};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::{self, Ty};
use rustc_span::Symbol;

dylint_linting::declare_late_lint! {
    /// ### What it does
    ///
    /// Detects usage of `schemars::schema_for!()` macro on GTS-wrapped structs.
    ///
    /// ### Why is this bad?
    ///
    /// GTS-wrapped structs (those using `#[struct_to_gts_schema]`) must use
    /// `gts_schema_with_refs_as_string()` for schema generation because:
    ///
    /// 1. **Performance**: It is static (computed at compile time), so it's faster
    /// 2. **Correct `$id`**: It automatically sets the correct `$id` field
    /// 3. **Proper `$ref`s**: It generates proper schema with `$ref` references,
    ///    while `schema_for!` inlines everything
    ///
    /// ### Example
    ///
    /// ```rust
    /// // Bad - uses schema_for! on a GTS struct
    /// #[struct_to_gts_schema(...)]
    /// pub struct MyPluginSpec { ... }
    ///
    /// let schema = schemars::schema_for!(MyPluginSpec);
    /// ```
    ///
    /// Use instead:
    ///
    /// ```rust
    /// // Good - uses GTS-provided method
    /// #[struct_to_gts_schema(...)]
    /// pub struct MyPluginSpec { ... }
    ///
    /// let schema = MyPluginSpec::gts_schema_with_refs_as_string();
    /// ```
    pub DE0902_NO_SCHEMA_FOR_ON_GTS_STRUCTS,
    Deny,
    "GTS structs must use gts_schema_with_refs_as_string() instead of schema_for!() (DE0902)"
}

/// Check if a type has the gts_schema_with_refs_as_string method.
/// GTS types generated by `#[struct_to_gts_schema]` have this method.
fn is_gts_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
    // Get the ADT (struct/enum) definition if this is one
    if let ty::Adt(adt_def, _) = ty.kind() {
        let def_id = adt_def.did();

        // Check if this type has a method named gts_schema_with_refs_as_string
        let gts_method = Symbol::intern("gts_schema_with_refs_as_string");

        for item in cx.tcx.inherent_impls(def_id).iter() {
            for &assoc_item_def_id in cx.tcx.associated_item_def_ids(*item) {
                let assoc_item = cx.tcx.associated_item(assoc_item_def_id);
                if assoc_item.name() == gts_method {
                    return true;
                }
            }
        }
    }
    false
}

/// Extract type name for error message
fn get_type_name<'tcx>(ty: Ty<'tcx>) -> String {
    if let ty::Adt(adt_def, _) = ty.kind() {
        adt_def.variant(0u32.into()).name.to_string()
    } else {
        ty.to_string()
    }
}

impl<'tcx> LateLintPass<'tcx> for De0902NoSchemaForOnGtsStructs {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
        // The schema_for! macro expands to:
        // schemars::gen::SchemaGenerator::default().into_root_schema_for::<Type>()
        //
        // We look for method calls to `into_root_schema_for` with a GTS type as type parameter.

        if let ExprKind::MethodCall(segment, _receiver, _args, _span) = expr.kind {
            let method_name = segment.ident.name.as_str();

            // Check for into_root_schema_for (from schema_for! macro expansion)
            if method_name == "into_root_schema_for" {
                // Only report if this comes from a schema_for! macro invocation,
                // not from derive macro expansions
                let callsite_span = expr.span.source_callsite();

                // Check if the callsite is a schema_for! macro call by looking at the source
                let source_map = cx.sess().source_map();
                let snippet = source_map
                    .span_to_snippet(callsite_span)
                    .unwrap_or_default();
                if !snippet.contains("schema_for!") {
                    return;
                }

                // Check the generic type argument
                if let Some(args) = segment.args {
                    for arg in args.args {
                        if let GenericArg::Type(hir_ty) = arg
                            && let Some(ty) = cx.typeck_results().node_type_opt(hir_ty.hir_id)
                            && is_gts_type(cx, ty)
                        {
                            let type_name = get_type_name(ty);
                            span_lint_and_then(
                                cx,
                                DE0902_NO_SCHEMA_FOR_ON_GTS_STRUCTS,
                                callsite_span,
                                format!(
                                    "do not use `schema_for!({})` on GTS-wrapped struct (DE0902)",
                                    type_name
                                ),
                                |diag| {
                                    diag.help(format!(
                                        "use `{}::gts_schema_with_refs_as_string()` instead for proper `$id` and `$ref` handling",
                                        type_name
                                    ));
                                },
                            );
                            return;
                        }
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn ui_examples() {
        dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME"));
    }

    #[test]
    fn test_comment_annotations_match_stderr() {
        let ui_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui");
        lint_utils::test_comment_annotations_match_stderr(
            &ui_dir,
            "DE0902",
            "schema_for on GTS struct",
        );
    }
}
