use oxc_ast::{
    AstKind,
    ast::{
        Argument, BindingPattern, CallExpression, Expression, FormalParameterRest,
        FormalParameters, FunctionBody, MethodDefinition, Statement, TSAccessibility,
    },
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

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

/// ```js
/// class A { constructor(){} }
/// ```
fn no_empty_constructor(constructor_span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Empty constructors are unnecessary")
        .with_label(constructor_span)
        .with_help("Remove the constructor or add code to it.")
}

/// ```js
/// class A { }
/// class B extends A {
///     constructor() {
///         super();
///     }
/// }
/// ```
fn no_redundant_super_call(constructor_span: Span, super_span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Redundant super call in constructor")
        .with_labels([
            constructor_span.primary_label("This constructor is unnecessary,"),
            super_span.label("because it only passes arguments through to the superclass"),
        ])
        .with_note("Subclasses automatically use the constructor of their superclass, making this redundant.")
        .with_help("Remove this constructor or add code to it.")
}

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

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Disallow constructors that can be safely removed without changing how the class works.
    ///
    /// ### Why is this bad?
    ///
    /// ES2015 provides a default class constructor if one is not specified. As
    /// such, it is unnecessary to provide an empty constructor or one that
    /// simply delegates into its parent class.
    ///
    /// ::: warning
    /// Caveat: This lint rule will report on constructors whose sole purpose
    /// is to change the visibility of a parent constructor, or to expose parameter
    /// properties with modifiers. This is because the rule does not have type
    /// information to determine if the parent constructor is `public`, `protected`,
    /// or `private`.
    /// :::
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```javascript
    /// class A {
    ///   constructor() {}
    /// }
    ///
    /// class B extends A {
    ///   constructor(...args) {
    ///     super(...args);
    ///   }
    /// }
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```javascript
    /// class A { }
    ///
    /// class B {
    ///   constructor() {
    ///     doSomething();
    ///   }
    /// }
    ///
    /// class C extends A {
    ///   constructor() {
    ///     super('foo');
    ///   }
    /// }
    ///
    /// class D extends A {
    ///   constructor() {
    ///     super();
    ///     doSomething();
    ///   }
    /// }
    /// ```
    NoUselessConstructor,
    eslint,
    suspicious,
    suggestion,
    version = "0.4.4",
    short_description = "Disallow constructors that can be safely removed without changing how the class works.",
);

impl Rule for NoUselessConstructor {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        let AstKind::MethodDefinition(constructor) = node.kind() else {
            return;
        };
        if !constructor.kind.is_constructor() {
            return;
        }
        let Some(body) = &constructor.value.body else {
            return;
        };

        let class = ctx.nodes().ancestors(node.id()).find_map(|parent| parent.kind().as_class());
        debug_assert!(class.is_some(), "Found a constructor outside of a class definition");
        let Some(class) = class else {
            return;
        };

        match &constructor.accessibility {
            Some(TSAccessibility::Private | TSAccessibility::Protected) => {
                return;
            }
            Some(TSAccessibility::Public) if class.super_class.is_some() => {
                return;
            }
            _ => {}
        }

        if class.declare {
            return;
        }

        if class.super_class.is_none() {
            lint_empty_constructor(ctx, constructor, body);
        } else {
            lint_redundant_super_call(ctx, constructor, body);
        }
    }
}

// Check for an empty constructor in a class without a superclass.
fn lint_empty_constructor<'a>(
    ctx: &LintContext<'a>,
    constructor: &MethodDefinition<'a>,
    body: &FunctionBody<'a>,
) {
    if !body.statements.is_empty() {
        return;
    }

    // allow constructors with parameter properties since they actually declare
    // class members
    if constructor
        .value
        .params
        .items
        .iter()
        .any(|param| param.has_modifier() || !param.decorators.is_empty())
    {
        return;
    }

    ctx.diagnostic_with_suggestion(no_empty_constructor(constructor.span), |fixer| {
        fixer.delete_range(constructor.span)
    });
}

fn lint_redundant_super_call<'a>(
    ctx: &LintContext<'a>,
    constructor: &MethodDefinition<'a>,
    body: &FunctionBody<'a>,
) {
    let Some(super_call) = is_single_super_call(body) else {
        return;
    };

    let params = &constructor.value.params;
    let super_args = &super_call.arguments;

    if is_only_simple_params(params)
        && !is_overriding(params)
        && !has_decorated_params(params)
        && (is_spread_arguments(super_args) || is_passing_through(params, super_args))
    {
        ctx.diagnostic_with_suggestion(
            no_redundant_super_call(constructor.key.span(), super_call.span),
            |fixer| fixer.delete_range(constructor.span),
        );
    }
}

fn is_overriding(params: &FormalParameters) -> bool {
    params.items.iter().any(|param| param.r#override)
}

fn has_decorated_params(params: &FormalParameters) -> bool {
    params.items.iter().any(|param| !param.decorators.is_empty())
}

/// Check if a function body only contains a single `super()` call. Ignores directives.
///
/// Returns the call expression if the body contains a single `super()` call, otherwise [`None`].
fn is_single_super_call<'a, 'f>(body: &'f FunctionBody<'a>) -> Option<&'f CallExpression<'a>> {
    if body.statements.len() != 1 {
        return None;
    }
    let Statement::ExpressionStatement(expr) = &body.statements[0] else { return None };
    let Expression::CallExpression(call) = &expr.expression else { return None };

    if call.callee.is_super() { Some(call) } else { None }
}

/// Returns `false` if any parameter is an array/object unpacking binding or has
/// a default value (initializer).
fn is_only_simple_params(params: &FormalParameters) -> bool {
    params.iter_bindings().all(BindingPattern::is_binding_identifier)
        && params.items.iter().all(|param| param.initializer.is_none())
}

/// Check if the super call is just spreading the `arguments` object, i.e., `super(...arguments)`.
///
/// Returns `true` only if there is exactly one spread argument that is the `arguments` identifier.
/// This ensures we don't flag constructors that transform arguments before passing to super,
/// such as `super(...args.map(x => x))`.
fn is_spread_arguments(super_args: &[Argument<'_>]) -> bool {
    if super_args.len() == 1
        && let Argument::SpreadElement(spread) = &super_args[0]
        && spread.argument.is_specific_id("arguments")
    {
        return true;
    }

    false
}

fn is_passing_through<'a>(
    constructor_params: &FormalParameters<'a>,
    super_args: &[Argument<'a>],
) -> bool {
    if constructor_params.parameters_count() != super_args.len() {
        return false;
    }
    if let Some(rest) = &constructor_params.rest {
        let all_but_last = super_args.iter().take(super_args.len() - 1);
        let Some(last_arg) = super_args.iter().next_back() else { return false };
        constructor_params
            .items
            .iter()
            .zip(all_but_last)
            .all(|(param, arg)| is_matching_identifier_pair(&param.pattern, arg))
            && is_matching_rest_spread_pair(rest, last_arg)
    } else {
        constructor_params
            .iter_bindings()
            .zip(super_args)
            .all(|(param, arg)| is_matching_identifier_pair(param, arg))
    }
}

fn is_matching_identifier_pair<'a>(param: &BindingPattern<'a>, arg: &Argument<'a>) -> bool {
    match (&param, arg) {
        (BindingPattern::BindingIdentifier(param), Argument::Identifier(arg)) => {
            param.name == arg.name
        }
        _ => false,
    }
}
fn is_matching_rest_spread_pair<'a>(rest: &FormalParameterRest<'a>, arg: &Argument<'a>) -> bool {
    match (&rest.rest.argument, arg) {
        (BindingPattern::BindingIdentifier(param), Argument::SpreadElement(spread)) => {
            matches!(&spread.argument, Expression::Identifier(ident) if param.name == ident.name)
        }
        _ => false,
    }
}

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

    let pass = vec![
        "class A { }",
        "class A { constructor(){ doSomething(); } }",
        "class A extends B { constructor(){} }",
        "class A extends B { constructor(){ super('foo'); } }",
        "class A extends B { constructor(foo, bar){ super(foo, bar, 1); } }",
        "class A extends B { constructor(){ super(); doSomething(); } }",
        "class A extends B { constructor(...args){ super(...args); doSomething(); } }",
        "class A { dummyMethod(){ doSomething(); } }",
        "class A extends B.C { constructor() { super(foo); } }",
        "class A extends B.C { constructor([a, b, c]) { super(...arguments); } }",
        "class A extends B.C { constructor(a = f()) { super(...arguments); } }",
        "class A extends B { constructor(a, b, c) { super(a, b); } }",
        "class A extends B { constructor(foo, bar){ super(foo); } }",
        "class A extends B { constructor(test) { super(); } }",
        "class A extends B { constructor() { foo; } }",
        "class A extends B { constructor(foo, bar) { super(bar); } }",
        // ts
        "declare class A { constructor(options: any); }", // {                "parser": require("../../fixtures/parsers/typescript-parsers/declare-class")            }
        "class A { private constructor() {} }",
        "class A { protected constructor() {} }",
        "class A { constructor(private x: number) {} }",
        "class A { constructor(public x: number) {} }",
        "class A { constructor(protected x: number) {} }",
        "class A { constructor(readonly x: number) {} }",
        "class A { constructor(private readonly x: number) {} }",
        "class A extends B { constructor(override x: number) { super(x); } }",
        "
        class A {
            protected foo: number | undefined;
            constructor(foo?: number) {
                this.foo = foo;
            }
        }
        class B extends A {
            protected foo: number;
            constructor(foo: number = 0) {
                super(foo);
            }
        }
        ",
        "
        class A {
            protected foo: number | undefined;
            constructor(foo?: number) {
                this.foo = foo;
            }
        }
        class B extends A {
            constructor(foo?: number) {
                super(foo ?? 0);
            }
        }
        ",
        // TODO: type aware linting :(
        // "
        // class A {
        //     protected foo: string;
        //     constructor(foo: string) {
        //         this.foo = foo;
        //     }
        // }
        // class B extends A {
        //     constructor(foo: 'a' | 'b') {
        //         super(foo);
        //     }
        // }
        // ",
        // Argument transformation: constructor is not useless when arguments are processed/transformed
        // https://github.com/oxc-project/oxc/issues/17469
        "class A extends B { constructor(...args) { super(...args.map(x => x)); } }",
        "class A extends B { constructor(...args) { super(...args.filter(x => x)); } }",
        "class A extends B { constructor(...args) { super(...args.slice(1)); } }",
        "class A extends B { constructor(...args) { super(...transform(args)); } }",
        "class A extends B { constructor(...args) { super(...[...args, extra]); } }",
    ];

    let pass_typescript = vec![
        "class A {}",
        "
			class A {
			  constructor() {
			    doSomething();
			  }
			}
			    ",
        "
			class A extends B {
			  constructor() {}
			}
			    ",
        "
			class A extends B {
			  constructor() {
			    super('foo');
			  }
			}
			    ",
        "
			class A extends B {
			  constructor(foo, bar) {
			    super(foo, bar, 1);
			  }
			}
			    ",
        "
			class A extends B {
			  constructor() {
			    super();
			    doSomething();
			  }
			}
			    ",
        "
			class A extends B {
			  constructor(...args) {
			    super(...args);
			    doSomething();
			  }
			}
			    ",
        "
			class A {
			  dummyMethod() {
			    doSomething();
			  }
			}
			    ",
        "
			class A extends B.C {
			  constructor() {
			    super(foo);
			  }
			}
			    ",
        "
			class A extends B.C {
			  constructor([a, b, c]) {
			    super(...arguments);
			  }
			}
			    ",
        "
			class A extends B.C {
			  constructor(a = f()) {
			    super(...arguments);
			  }
			}
			    ",
        "
			class A extends B {
			  constructor(a, b, c) {
			    super(a, b);
			  }
			}
			    ",
        "
			class A extends B {
			  constructor(foo, bar) {
			    super(foo);
			  }
			}
			    ",
        "
			class A extends B {
			  constructor(test) {
			    super();
			  }
			}
			    ",
        "
			class A extends B {
			  constructor() {
			    foo;
			  }
			}
			    ",
        "
			class A extends B {
			  constructor(foo, bar) {
			    super(bar);
			  }
			}
			    ",
        "
			declare class A {
			  constructor();
			}
			    ",
        "
			class A {
			  constructor();
			}
			    ",
        "
			abstract class A {
			  constructor();
			}
			    ",
        "
			class A {
			  constructor(private name: string) {}
			}
			    ",
        "
			class A {
			  constructor(public name: string) {}
			}
			    ",
        "
			class A {
			  constructor(protected name: string) {}
			}
			    ",
        "
			class A {
			  private constructor() {}
			}
			    ",
        "
			class A {
			  protected constructor() {}
			}
			    ",
        "
			class A extends B {
			  public constructor() {}
			}
			    ",
        "
			class A extends B {
			  protected constructor(foo, bar) {
			    super(bar);
			  }
			}
			    ",
        "
			class A extends B {
			  private constructor(foo, bar) {
			    super(bar);
			  }
			}
			    ",
        "
			class A extends B {
			  public constructor(foo) {
			    super(foo);
			  }
			}
			    ",
        "
			class A extends B {
			  public constructor(foo) {}
			}
			    ",
        "
			class A {
			  constructor(foo);
			}
			    ",
        "
			class A extends Object {
			  constructor(@Foo foo: string) {
			    super(foo);
			  }
			}
			    ",
        "
			class A extends Object {
			  constructor(foo: string, @Bar() bar) {
			    super(foo, bar);
			  }
			}
			    ",
    ];

    let fail = vec![
        "class A { constructor(){} }",
        "class A { 'constructor'(){} }",
        "class A extends B { constructor() { super(); } }",
        "class A extends B {
    constructor() {
        super();
    }
}",
        "class A extends B { constructor(foo){ super(foo); } }",
        "class A extends B { constructor(foo, bar){ super(foo, bar); } }",
        "class A extends B { constructor(...args){ super(...args); } }",
        "class A extends B.C { constructor() { super(...arguments); } }",
        "class A extends B { constructor(a, b, ...c) { super(...arguments); } }",
        "class A extends B { constructor(a, b, ...c) { super(a, b, ...c); } }",
        // ts
        "class A { public constructor(){} }",
    ];

    let fail_typescript = vec![
        "
			class A {
			  constructor() {}
			}
			      ",
        "
			class A extends B {
			  constructor() {
			    super();
			  }
			}
			      ",
        "
			class A extends B {
			  constructor(foo) {
			    super(foo);
			  }
			}
			      ",
        "
			class A extends B {
			  constructor(foo, bar) {
			    super(foo, bar);
			  }
			}
			      ",
        "
			class A extends B {
			  constructor(...args) {
			    super(...args);
			  }
			}
			      ",
        "
			class A extends B.C {
			  constructor() {
			    super(...arguments);
			  }
			}
			      ",
        "
			class A extends B {
			  constructor(a, b, ...c) {
			    super(...arguments);
			  }
			}
			      ",
        "
			class A extends B {
			  constructor(a, b, ...c) {
			    super(a, b, ...c);
			  }
			}
			      ",
        "
			class A {
			  public constructor() {}
			}
			      ",
    ];

    let fix = vec![
        ("class A { constructor(){} }", "class A {  }"),
        (
            "class A extends B { constructor() { super(); } foo() { bar(); } }",
            "class A extends B {  foo() { bar(); } }",
        ),
    ];

    let pass = [pass, pass_typescript].concat();
    let fail = [fail, fail_typescript].concat();

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