use oxc_ast::{
    AstKind,
    ast::{BindingPattern, Expression},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{AstNode, context::LintContext, rule::Rule};

fn identifier(span: Span, param: &str) -> OxcDiagnostic {
    OxcDiagnostic::warn(format!("Do not use an object literal as default for parameter `{param}`."))
        .with_label(span)
}

fn non_identifier(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Do not use an object literal as default").with_label(span)
}

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

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Disallow the use of an object literal as a default value for a parameter.
    ///
    /// ### Why is this bad?
    ///
    /// Default parameters should not be passed to a function through an object literal. The `foo = {a: false}` parameter works fine if only used with one option. As soon as additional options are added, you risk replacing the whole `foo = {a: false, b: true}` object when passing only one option: `{a: true}`. For this reason, object destructuring should be used instead.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```javascript
    /// function foo(foo = {a: false}) {}
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```javascript
    /// function foo({a = false} = {}) {}
    /// ```
    NoObjectAsDefaultParameter,
    unicorn,
    pedantic,
    version = "0.0.16",
    short_description = "Disallow the use of an object literal as a default value for a parameter.",
);
impl Rule for NoObjectAsDefaultParameter {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        let AstKind::FormalParameter(formal_param) = node.kind() else {
            return;
        };

        let Some(init) = &formal_param.initializer else {
            return;
        };

        let init = init.get_inner_expression();

        let Expression::ObjectExpression(object_expr) = init else {
            return;
        };

        if object_expr.properties.is_empty() {
            return;
        }

        if let BindingPattern::BindingIdentifier(binding_id) = &formal_param.pattern {
            ctx.diagnostic(identifier(object_expr.span, &binding_id.name));
            return;
        }

        ctx.diagnostic(non_identifier(object_expr.span));
    }
}

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

    let pass = vec![
        "const abc = {};",
        "const abc = {foo: 123};",
        "function abc(foo) {}",
        "function abc(foo = null) {}",
        "function abc(foo = undefined) {}",
        "function abc(foo = 123) {}",
        "function abc(foo = true) {}",
        r#"function abc(foo = "bar") {}"#,
        r#"function abc(foo = 123, bar = "foo") {}"#,
        "function abc(foo = {}) {}",
        "function abc({foo = 123} = {}) {}",
        "(function abc() {})(foo = {a: 123})",
        "const abc = foo => {};",
        "const abc = (foo = null) => {};",
        "const abc = (foo = undefined) => {};",
        "const abc = (foo = 123) => {};",
        "const abc = (foo = true) => {};",
        r#"const abc = (foo = "bar") => {};"#,
        r#"const abc = (foo = ("bar")) => {};"#,
        r#"const abc = (foo = 123, bar = "foo") => {};"#,
        "const abc = (foo = {}) => {};",
        r#"const abc = ({a = true, b = "foo"}) => {};"#,
        "const abc = function(foo = 123) {}",
        "const {abc = {foo: 123}} = bar;",
        r#"const {abc = {null: "baz"}} = bar;"#,
        "const {abc = {foo: undefined}} = undefined;",
        "const abc = ([{foo = false, bar = 123}]) => {};",
        "const abc = ({foo = {a: 123}}) => {};",
        "const abc = ({foo = ({a: 123})}) => {};",
        "const abc = ([foo = {a: 123}]) => {};",
        "const abc = ({foo: bar = {a: 123}}) => {};",
        "const abc = () => (foo = {a: 123});",
        "class A {
                [foo = {a: 123}]() {}
            }",
        "class A extends (foo = {a: 123}) {
                a() {}
            }",
    ];

    let fail = vec![
        "function abc(foo = {a: 123}) {}",
        "async function * abc(foo = {a: 123}) {}",
        "function abc(foo = {a: false}) {}",
        "function abc(foo = ({a: false})) {}",
        r#"function abc(foo = {a: "bar"}) {}"#,
        r#"function abc(foo = {a: "bar", b: {c: true}}) {}"#,
        "const abc = (foo = {a: false}) => {};",
        "const abc = (foo = {a: 123, b: false}) => {};",
        r#"const abc = (foo = {a: false, b: 1, c: "test", d: null}) => {};"#,
        "const abc = function(foo = {a: 123}) {}",
        "class A {
                abc(foo = {a: 123}) {}
            }",
        "class A {
                constructor(foo = {a: 123}) {}
            }",
        // This one is a syntax error and so should be excluded from the failure cases.
        // "class A {
        //         set abc(foo = {a: 123}) {}
        //     }",
        "class A {
                static abc(foo = {a: 123}) {}
            }",
        "class A {
                * abc(foo = {a: 123}) {}
            }",
        "class A {
                static async * abc(foo = {a: 123}) {}
            }",
        "class A {
                [foo = {a: 123}](foo = {a: 123}) {}
            }",
        "const A = class {
                abc(foo = {a: 123}) {}
            }",
        "object = {
                abc(foo = {a: 123}) {}
            };",
        "const A = class {
                abc({a} = {a: 123}) {}
            }",
        "/**/function abc(foo = {a: 123}) {}",
        "const abc = (foo = {a: false}) => {};",
        "function abc({a} = {a: 123}) {}",
        "function abc([a] = {a: 123}) {}",
    ];

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