use oxc_allocator::{GetAddress, UnstableAddress};
use oxc_ast::{
    AstKind,
    ast::{Expression, MemberExpression},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

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

fn prefer_regexp_test_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`.")
        .with_help("`RegExp#test()` exclusively returns a boolean and therefore is more efficient.")
        .with_label(span)
}

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

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Prefers `RegExp#test()` over `String#match()` and `String#exec()`.
    ///
    /// ### Why is this bad?
    ///
    /// When you want to know whether a pattern is found in a string, use
    /// [`RegExp#test()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test)
    /// instead of [`String#match()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match)
    /// or [`RegExp#exec()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec),
    /// as it exclusively returns a boolean and therefore is more efficient.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```javascript
    /// if (string.match(/unicorn/)) { }
    /// if (/unicorn/.exec(string)) {}
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```javascript
    /// if (/unicorn/.test(string)) {}
    /// Boolean(string.match(/unicorn/))
    /// ```
    PreferRegexpTest,
    unicorn,
    pedantic,
    fix,
    version = "0.0.16",
    short_description = "Prefers `RegExp#test()` over `String#match()` and `String#exec()`.",
);

impl Rule for PreferRegexpTest {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        let AstKind::CallExpression(call_expr) = node.kind() else {
            return;
        };

        let Some(member_expr) = call_expr.callee.get_member_expr() else {
            return;
        };

        if call_expr.optional || call_expr.arguments.len() != 1 {
            return;
        }

        if call_expr.arguments[0].is_spread() {
            return;
        }

        let (span, name) = match member_expr {
            MemberExpression::StaticMemberExpression(v) => {
                if !matches!(v.property.name.as_str(), "match" | "exec") {
                    return;
                }
                (v.property.span, &v.property.name)
            }
            _ => return,
        };

        let Some(parent) = outermost_paren_parent(node, ctx) else {
            return;
        };

        match parent.kind() {
            AstKind::ForStatement(for_stmt) => {
                let Some(test) = &for_stmt.test else { return };

                let Expression::CallExpression(call_expr2) = test else {
                    return;
                };

                // Check if the `test` of the for statement is the same node as the call expression.
                if call_expr2.address() != call_expr.unstable_address() {
                    return;
                }
            }
            AstKind::ConditionalExpression(conditional_expr) => {
                let Expression::CallExpression(call_expr2) = &conditional_expr.test else {
                    return;
                };

                // Check if the `test` of the conditional expression is the same node as the call expression.
                if call_expr2.address() != call_expr.unstable_address() {
                    return;
                }
            }
            AstKind::CallExpression(call_expr) => {
                let Expression::Identifier(ident) = &call_expr.callee else {
                    return;
                };

                if ident.name.as_str() != "Boolean" {
                    return;
                }
            }
            AstKind::WhileStatement(_)
            | AstKind::DoWhileStatement(_)
            | AstKind::IfStatement(_)
            | AstKind::UnaryExpression(_) => {}
            _ => return,
        }

        match name.as_str() {
            "match" => {
                if member_expr.object().is_literal()
                    && !matches!(member_expr.object(), Expression::RegExpLiteral(_))
                {
                    return;
                }

                if let Some(expr) = call_expr.arguments[0].as_expression()
                    && expr.is_literal()
                    && !matches!(expr, Expression::RegExpLiteral(_))
                {
                    return;
                }
            }
            "exec" => {
                if member_expr.object().is_literal()
                    && !matches!(member_expr.object(), Expression::RegExpLiteral(_))
                {
                    return;
                }
            }
            _ => unreachable!("expected match or test, got: {:?}", name),
        }

        ctx.diagnostic_with_fix(prefer_regexp_test_diagnostic(span), |fixer| {
            if name.as_str() == "exec" {
                return fixer.replace(span, "test");
            }
            let mut fix = fixer.new_fix_with_capacity(3);

            fix.push(fixer.replace(span, "test"));
            fix.push(fixer.replace_with(&call_expr.arguments[0], member_expr.object()));
            fix.push(fixer.replace_with(member_expr.object(), &call_expr.arguments[0]));

            fix.with_message("Replace with `RegExp.test()`")
        });
    }
}

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

    let pass = vec![
        "const bar = !re.test(foo)",
        "const matches = foo.match(re) || []",
        "const matches = foo.match(re)",
        "const matches = re.exec(foo)",
        "while (foo = re.exec(bar)) {}",
        "while ((foo = re.exec(bar))) {}",
        "if (foo.notMatch(re)) {}",
        "if (re.notExec(foo)) {}",
        "if (foo.match) {}",
        "if (re.exec) {}",
        "if (foo[match](re)) {}",
        "if (re[exec](foo)) {}",
        r#"if (foo["match"](re)) {}"#,
        r#"if (re["exec"](foo)) {}"#,
        "if (match(re)) {}",
        "if (exec(foo)) {}",
        "if (foo.match()) {}",
        "if (re.exec()) {}",
        "if (foo.match(re, another)) {}",
        "if (re.exec(foo, another)) {}",
        "if (foo.match(...[regexp])) {}",
        "if (re.exec(...[string])) {}",
        "if (foo.match(1)) {}",
        r#"if (foo.match("1")) {}"#,
        "if (foo.match(null)) {}",
        "if (foo.match(1n)) {}",
        "if (foo.match(true)) {}",
    ];

    let fail = vec![
        "const re = /a/; const bar = !foo.match(re)",
        "const re = /a/; const bar = Boolean(foo.match(re))",
        "const re = /a/; if (foo.match(re)) {}",
        "const re = /a/; const bar = foo.match(re) ? 1 : 2",
        "const re = /a/; while (foo.match(re)) foo = foo.slice(1);",
        "const re = /a/; do {foo = foo.slice(1)} while (foo.match(re));",
        "const re = /a/; for (; foo.match(re); ) foo = foo.slice(1);",
        "const re = /a/; const bar = !re.exec(foo)",
        "const re = /a/; const bar = Boolean(re.exec(foo))",
        "const re = /a/; if (re.exec(foo)) {}",
        "const re = /a/; const bar = re.exec(foo) ? 1 : 2",
        "const re = /a/; while (re.exec(foo)) foo = foo.slice(1);",
        "const re = /a/; do {foo = foo.slice(1)} while (re.exec(foo));",
        "const re = /a/; for (; re.exec(foo); ) foo = foo.slice(1);",
        "const re = /a/; if ((0, foo).match(re)) {}",
        "const re = /a/; if ((0, foo).match((re))) {}",
        "const re = /a/; if ((foo).match(re)) {}",
        "const re = /a/; if ((foo).match((re))) {}",
        "if (foo.match(/re/)) {}",
        "const re = /a/; if (foo.match(re)) {}",
        "const bar = {bar: /a/}; if (foo.match(bar.baz)) {}",
        "if (foo.match(bar.baz())) {}",
        r#"if (foo.match(new RegExp("re", "g"))) {}"#,
        "if (foo.match(new SomeRegExp())) {}",
        "if (foo.match(new SomeRegExp)) {}",
        "if (foo.match(bar?.baz)) {}",
        "if (foo.match(bar?.baz())) {}",
        "if (foo.match(bar || baz)) {}",
        "async function a() {
                if (foo.match(await bar())) {}
            }",
        "if ((foo).match(/re/)) {}",
        "if ((foo).match(new SomeRegExp)) {}",
        "if ((foo).match(bar?.baz)) {}",
        "if ((foo).match(bar?.baz())) {}",
        "const bar = false; const baz = /a/; if ((foo).match(bar || baz)) {}",
        "async function a() {
                if ((foo).match(await bar())) {}
            }",
        "const re = [/a/]; if (foo.match([re][0])) {}",
        "async function a() {
                if (
                    /* 1 */ foo() /* 2 */
                        ./* 3 */ match /* 4 */ (
                            /* 5 */ await /* 6 */ bar() /* 7 */
                            ,
                            /* 8 */
                        )
                ) {}
            }",
        r"const string = '[.!?]\\\\s*$';
            if (foo.match(string)) {
            }",
        r"const regex = new RegExp('[.!?]\\\\s*$');
            if (foo.match(regex)) {}",
        "if (foo.match(unknown)) {}",
        "if (foo.match(/a/g));",
        "if (foo.match(/a/y));",
        "if (foo.match(/a/gy));",
        "if (foo.match(/a/ig));",
        r#"if (foo.match(new RegExp("a", "g")));"#,
        "if (/a/g.exec(foo));",
        "if (/a/y.exec(foo));",
        "if (/a/gy.exec(foo));",
        "if (/a/yi.exec(foo));",
        r#"if (new RegExp("a", "g").exec(foo));"#,
        r#"if (new RegExp("a", "y").exec(foo));"#,
        "const regex = /weird/g;
            if (foo.match(regex));",
        "const regex = /weird/g;
            if (regex.exec(foo));",
        "const regex = /weird/y;
            if (regex.exec(foo));",
        "const regex = /weird/gyi;
            if (regex.exec(foo));",
        "let re = new RegExp('foo', 'g');
            if(str.match(re));",
        "!/a/u.exec(foo)",
        "!/a/v.exec(foo)",
    ];

    let fix = vec![
        ("const re = /a/; const bar = !foo.match(re)", "const re = /a/; const bar = !re.test(foo)"),
        (
            "const re = /a/; const bar = Boolean(foo.match(re))",
            "const re = /a/; const bar = Boolean(re.test(foo))",
        ),
        ("const re = /a/; if (foo.match(re)) {}", "const re = /a/; if (re.test(foo)) {}"),
        (
            "const re = /a/; const bar = foo.match(re) ? 1 : 2",
            "const re = /a/; const bar = re.test(foo) ? 1 : 2",
        ),
        (
            "const re = /a/; while (foo.match(re)) foo = foo.slice(1);",
            "const re = /a/; while (re.test(foo)) foo = foo.slice(1);",
        ),
        (
            "const re = /a/; do {foo = foo.slice(1)} while (foo.match(re));",
            "const re = /a/; do {foo = foo.slice(1)} while (re.test(foo));",
        ),
        (
            "const re = /a/; for (; foo.match(re); ) foo = foo.slice(1);",
            "const re = /a/; for (; re.test(foo); ) foo = foo.slice(1);",
        ),
        ("const re = /a/; const bar = !re.exec(foo)", "const re = /a/; const bar = !re.test(foo)"),
        (
            "const re = /a/; const bar = Boolean(re.exec(foo))",
            "const re = /a/; const bar = Boolean(re.test(foo))",
        ),
        ("const re = /a/; if (re.exec(foo)) {}", "const re = /a/; if (re.test(foo)) {}"),
        ("const re = /a/; if (someStr.match(re)) {}", "const re = /a/; if (re.test(someStr)) {}"),
    ];

    Tester::new(PreferRegexpTest::NAME, PreferRegexpTest::PLUGIN, pass, fail)
        .expect_fix(fix)
        .test_and_snapshot();
}
