use oxc_ast::{
    AstKind,
    ast::{Argument, JSXAttributeItem, ObjectPropertyKind},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

use crate::{
    AstNode,
    context::{ContextHost, LintContext},
    rule::Rule,
    utils::{has_jsx_prop, is_create_element_call},
};

fn no_danger_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Do not use `dangerouslySetInnerHTML` prop")
        .with_help("`dangerouslySetInnerHTML` is a way to inject HTML into your React component. This is dangerous because it can easily lead to XSS vulnerabilities.")
        .with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct NoDanger;

declare_oxc_lint!(
    /// ### What it does
    ///
    /// This rule prevents the use of `dangerouslySetInnerHTML` prop.
    ///
    /// ### Why is this bad?
    ///
    /// `dangerouslySetInnerHTML` is a way to inject HTML into your React
    /// component. This is dangerous because it can easily lead to XSS
    /// vulnerabilities.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```jsx
    /// import React from "react";
    ///
    /// const Hello = <div dangerouslySetInnerHTML={{ __html: "Hello World" }}></div>;
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```jsx
    /// import React from "react";
    ///
    /// const Hello = <div>Hello World</div>;
    /// ```
    NoDanger,
    react,
    restriction,
    version = "0.0.14",
    short_description = "This rule prevents the use of `dangerouslySetInnerHTML` prop.",
);

impl Rule for NoDanger {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::JSXElement(jsx_elem) => {
                if let Some(JSXAttributeItem::Attribute(prop)) =
                    has_jsx_prop(&jsx_elem.opening_element, "dangerouslySetInnerHTML")
                {
                    ctx.diagnostic(no_danger_diagnostic(prop.name.span()));
                }
            }
            AstKind::CallExpression(call_expr) => {
                if !is_create_element_call(call_expr) {
                    return;
                }

                let Some(props) = call_expr.arguments.get(1) else {
                    return;
                };

                let Argument::ObjectExpression(obj_expr) = props else {
                    return;
                };

                for prop in &obj_expr.properties {
                    if let ObjectPropertyKind::ObjectProperty(obj_prop) = prop
                        && let Some(prop_name) = obj_prop.key.static_name()
                        && prop_name == "dangerouslySetInnerHTML"
                    {
                        ctx.diagnostic(no_danger_diagnostic(obj_prop.key.span()));
                    }
                }
            }
            _ => {}
        }
    }

    fn should_run(&self, ctx: &ContextHost) -> bool {
        ctx.source_type().is_jsx()
    }
}

#[test]
fn test() {
    use crate::tester::Tester;

    let pass = vec![
        "<App />;",
        "<div className=\"bar\"></div>;",
        "React.createElement(\"div\", { className: \"bar\" });",
    ];

    let fail = vec![
        "<div dangerouslySetInnerHTML={{ __html: \"\" }}></div>;",
        "<button dangerouslySetInnerHTML={{ __html: \"baz\" }}>Foo</button>;",
        "React.createElement(\"div\", { dangerouslySetInnerHTML: { __html: \"\" } });",
        "React.createElement(\"button\", { dangerouslySetInnerHTML: { __html: \"baz\" } }, \"Foo\");",
    ];

    Tester::new(NoDanger::NAME, NoDanger::PLUGIN, pass, fail).test_and_snapshot();
}
