use oxc_ast::{
    AstKind,
    ast::{Argument, CallExpression, Expression, Statement},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_span::Span;
use oxc_str::Str;

use crate::{context::LintContext, fixer::RuleFixer, utils::get_node_name};

fn use_mock_shorthand(preferred_name: &str, span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Prefer mock resolved/rejected shorthands for promises.")
        .with_help(format!("Prefer {preferred_name:?}"))
        .with_label(span)
}

pub const DOCUMENTATION: &str = r"### What it does

When working with mocks of functions that return promises, Jest provides some
API sugar functions to reduce the amount of boilerplate you have to write.
These methods should be preferred when possible.

### Why is this bad?

Using generic mock functions like `mockImplementation(() => Promise.resolve())`
or `mockReturnValue(Promise.reject())` is more verbose and less readable than
Jest's specialized promise shorthands. The shorthand methods like
`mockResolvedValue()` and `mockRejectedValue()` are more expressive and
make the test intent clearer.

### Examples

Examples of **incorrect** code for this rule:
```javascript
jest.fn().mockImplementation(() => Promise.resolve(123));
jest
  .spyOn(fs.promises, 'readFile')
  .mockReturnValue(Promise.reject(new Error('oh noes!')));

myFunction
  .mockReturnValueOnce(Promise.resolve(42))
  .mockImplementationOnce(() => Promise.resolve(42))
  .mockReturnValue(Promise.reject(new Error('too many calls!')));
```

Examples of **correct** code for this rule:
```javascript
jest.fn().mockResolvedValue(123);
jest.spyOn(fs.promises, 'readFile').mockRejectedValue(new Error('oh noes!'));

myFunction
  .mockResolvedValueOnce(42)
  .mockResolvedValueOnce(42)
  .mockRejectedValue(new Error('too many calls!'));
```
";

pub fn run<'a>(node: &oxc_semantic::AstNode<'a>, ctx: &LintContext<'a>) {
    let AstKind::CallExpression(call_expr) = node.kind() else {
        return;
    };
    let Some(mem_expr) = call_expr.callee.as_member_expression() else {
        return;
    };

    if call_expr.arguments.is_empty() {
        return;
    }

    let Some((property_span, property_name)) = mem_expr.static_property_info() else {
        return;
    };
    let Some(expr) = call_expr.arguments.first().and_then(Argument::as_expression) else {
        return;
    };
    let is_once = property_name.ends_with("Once");

    if property_name.eq("mockReturnValue") || property_name.eq("mockReturnValueOnce") {
        report(is_once, property_span, None, expr, ctx);
    } else if property_name.eq("mockImplementation") || property_name.eq("mockImplementationOnce") {
        match expr {
            Expression::ArrowFunctionExpression(arrow_func) => {
                if !arrow_func.params.is_empty() {
                    return;
                }
                let Some(stmt) = arrow_func.body.statements.first() else {
                    return;
                };

                if let Some(expr) = arrow_func.get_expression() {
                    report(is_once, property_span, Some(arrow_func.span), expr, ctx);
                } else if let Statement::ReturnStatement(return_stmt) = stmt {
                    let Some(arg_expr) = &return_stmt.argument else {
                        return;
                    };
                    report(is_once, property_span, Some(arrow_func.span), arg_expr, ctx);
                }
            }
            Expression::FunctionExpression(func_expr) => {
                if !func_expr.params.is_empty() {
                    return;
                }
                let Some(func_body) = &func_expr.body else {
                    return;
                };
                let Some(stmt) = func_body.statements.first() else {
                    return;
                };
                let Statement::ReturnStatement(return_stmt) = stmt else {
                    return;
                };
                let Some(arg_expr) = &return_stmt.argument else {
                    return;
                };
                report(is_once, property_span, Some(func_expr.span), arg_expr, ctx);
            }
            _ => (),
        }
    }
}

fn report<'a>(
    is_once: bool,
    property_span: Span,
    arg_span: Option<Span>,
    arg_expr: &'a Expression<'a>,
    ctx: &LintContext<'a>,
) {
    let Expression::CallExpression(call_expr) = arg_expr else {
        return;
    };
    let arg_name = get_node_name(arg_expr);

    if !arg_name.eq("Promise.resolve") && !arg_name.eq("Promise.reject") {
        return;
    }

    let mock_promise_resolve = if is_once { "mockResolvedValueOnce" } else { "mockResolvedValue" };
    let mock_promise_reject = if is_once { "mockRejectedValueOnce" } else { "mockRejectedValue" };
    let prefer_name: &'static str =
        if arg_name.ends_with("reject") { mock_promise_reject } else { mock_promise_resolve };
    let fix_span = arg_span.unwrap_or(call_expr.span);

    if call_expr.arguments.len() <= 1 {
        ctx.diagnostic_with_fix(
            use_mock_shorthand(Str::from(prefer_name).as_str(), property_span),
            |fixer| {
                let content = fix(fixer, prefer_name, call_expr);
                let span = Span::new(property_span.start, fix_span.end);
                fixer.replace(span, content)
            },
        );
    } else {
        ctx.diagnostic(use_mock_shorthand(Str::from(prefer_name).as_str(), property_span));
    }
}

fn fix<'a>(
    fixer: RuleFixer<'_, 'a>,
    prefer_name: &'a str,
    call_expr: &CallExpression<'a>,
) -> String {
    let mut content = fixer.codegen();
    content.print_str(prefer_name);
    content.print_ascii_byte(b'(');
    if call_expr.arguments.is_empty() {
        content.print_str("undefined");
    } else {
        for argument in &call_expr.arguments {
            if let Some(expr) = argument.as_expression() {
                content.print_expression(expr);
            }
        }
    }
    content.into_source_text()
}
