use oxc_ast::{AstKind, ast::Expression};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use oxc_str::Str;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;

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

fn no_useless_computed_key_diagnostic(span: Span, raw: Option<Str>) -> OxcDiagnostic {
    // false positive, if we remove the closure, `borrowed data escapes outside of function `raw` escapes the function body here`
    #[expect(clippy::redundant_closure)]
    let key = raw.unwrap_or_else(|| Str::empty());
    OxcDiagnostic::warn(format!("Unnecessarily computed property `{key}` found."))
        .with_help("Replace the computed property with a plain identifier or string literal")
        .with_label(span)
}

#[derive(Debug, Clone, JsonSchema, Deserialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct NoUselessComputedKey {
    /// The `enforceForClassMembers` option controls whether the rule applies to
    /// class members (methods and properties).
    ///
    /// Examples of **correct** code for this rule with the `{ "enforceForClassMembers": false }` option:
    /// ```js
    /// class SomeClass {
    ///     ["foo"] = "bar";
    ///     [42] = "baz";
    ///     get ['b']() {}
    ///     set ['c'](value) {}
    ///     static ["foo"] = "bar";
    /// }
    /// ```
    enforce_for_class_members: bool,
}

impl Default for NoUselessComputedKey {
    fn default() -> Self {
        Self { enforce_for_class_members: true }
    }
}

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Disallow unnecessary computed property keys in objects and classes.
    ///
    /// ### Why is this bad?
    ///
    /// It’s unnecessary to use computed properties with literals such as:
    /// ```js
    /// const foo = {["a"]: "b"};
    /// ```
    ///
    /// The code can be rewritten as:
    /// ```js
    /// const foo = {"a": "b"};
    /// ```
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```js
    /// const a = { ['0']: 0 };
    /// const b = { ['0+1,234']: 0 };
    /// const c = { [0]: 0 };
    /// const e = { ['x']() {} };
    ///
    /// class Foo {
    ///     ["foo"] = "bar";
    ///     [0]() {}
    ///     static ["foo"] = "bar";
    ///     get ['b']() {}
    ///     set ['c'](value) {}
    /// }
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```js
    /// const a = { 'a': 0 };
    /// const b = { 0: 0 };
    /// const c = { x() {} };
    /// const e = { '0+1,234': 0 };
    ///
    /// class Foo {
    ///     "foo" = "bar";
    ///     0() {}
    ///     'a'() {}
    ///     static "foo" = "bar";
    /// }
    /// ```
    ///
    /// Examples of additional **correct** code for this rule:
    /// ```js
    ///
    /// const c = {
    ///     "__proto__": foo, // defines object's prototype
    ///     ["__proto__"]: bar // defines a property named "__proto__"
    /// };
    /// class Foo {
    ///     ["constructor"]; // instance field named "constructor"
    ///     "constructor"() {} // the constructor of this class
    ///     static ["constructor"]; // static field named "constructor"
    ///     static ["prototype"]; // runtime error, it would be a parsing error without `[]`
    /// }
    /// ```
    NoUselessComputedKey,
    eslint,
    style,
    conditional_fix,
    config = NoUselessComputedKey,
    version = "1.16.0",
    short_description = "Disallow unnecessary computed property keys in objects and classes.",
);

impl Rule for NoUselessComputedKey {
    fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
        serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
    }

    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::ObjectProperty(property) if property.computed => {
                if let Some(expr) =
                    property.key.as_expression().map(Expression::get_inner_expression)
                {
                    check_computed_class_member(
                        ctx,
                        property.key.span(),
                        property.span,
                        expr,
                        false,
                        &[],
                        &["__proto__"],
                    );
                }
            }
            AstKind::BindingProperty(binding_prop) if binding_prop.computed => {
                if let Some(expr) =
                    binding_prop.key.as_expression().map(Expression::get_inner_expression)
                {
                    check_computed_class_member(
                        ctx,
                        binding_prop.span,
                        binding_prop.span,
                        expr,
                        false,
                        &[],
                        &[],
                    );
                }
            }
            AstKind::PropertyDefinition(prop_def)
                if self.enforce_for_class_members && prop_def.computed =>
            {
                if let Some(expr) =
                    prop_def.key.as_expression().map(Expression::get_inner_expression)
                {
                    check_computed_class_member(
                        ctx,
                        prop_def.key.span(),
                        prop_def.span,
                        expr,
                        prop_def.r#static,
                        &["prototype", "constructor"],
                        &["constructor"],
                    );
                }
            }
            AstKind::MethodDefinition(method_def)
                if self.enforce_for_class_members && method_def.computed =>
            {
                if let Some(expr) =
                    method_def.key.as_expression().map(Expression::get_inner_expression)
                {
                    check_computed_class_member(
                        ctx,
                        method_def.span,
                        method_def.span,
                        expr,
                        method_def.r#static,
                        &["prototype"],
                        &["constructor"],
                    );
                }
            }
            _ => {}
        }
    }
}

fn check_computed_class_member(
    ctx: &LintContext<'_>,
    diagnostic_span: Span,
    member_span: Span,
    expr: &Expression,
    is_static: bool,
    allow_static: &[&str],
    allow_non_static: &[&str],
) {
    match expr {
        Expression::StringLiteral(lit) => {
            let key_name = lit.value.as_str();
            let allowed = if is_static {
                allow_static.contains(&key_name)
            } else {
                allow_non_static.contains(&key_name)
            };
            if !allowed {
                report_useless_computed_key(
                    ctx,
                    diagnostic_span,
                    member_span,
                    expr.span(),
                    lit.raw,
                );
            }
        }
        Expression::NumericLiteral(number_lit) => {
            report_useless_computed_key(
                ctx,
                diagnostic_span,
                member_span,
                expr.span(),
                number_lit.raw,
            );
        }
        _ => {}
    }
}

fn report_useless_computed_key(
    ctx: &LintContext<'_>,
    diagnostic_span: Span,
    member_span: Span,
    key_span: Span,
    raw: Option<Str>,
) {
    ctx.diagnostic_with_fix(no_useless_computed_key_diagnostic(diagnostic_span, raw), |fixer| {
        let Some(raw) = raw else {
            return fixer.noop();
        };
        let Some(computed_key_span) = get_computed_key_span(ctx, member_span, key_span) else {
            return fixer.noop();
        };
        if ctx.has_comments_between(computed_key_span) {
            return fixer.noop();
        }

        let mut replacement = raw.to_string();
        pad_fix_with_token_boundary(ctx.source_text(), computed_key_span, &mut replacement);
        fixer.replace(computed_key_span, replacement)
    });
}

fn get_computed_key_span(ctx: &LintContext<'_>, member_span: Span, key_span: Span) -> Option<Span> {
    let left_bracket =
        member_span.start + ctx.find_prev_token_within(member_span.start, key_span.start, "[")?;
    let right_bracket =
        key_span.end + ctx.find_next_token_within(key_span.end, member_span.end, "]")?;
    Some(Span::new(left_bracket, right_bracket + 1))
}

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

    let pass = vec![
        ("({ 'a': 0, b(){} })", None),
        ("({ [x]: 0 });", None),
        ("({ a: 0, [b](){} })", None),
        ("({ ['__proto__']: [] })", None),
        ("var { 'a': foo } = obj", None),
        ("var { [a]: b } = obj;", None),
        ("var { a } = obj;", None),
        ("var { a: a } = obj;", None),
        ("var { a: b } = obj;", None),
        ("class Foo { a() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        ("class Foo { 'a'() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        ("class Foo { [x]() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        (
            "class Foo { ['constructor']() {} }",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        (
            "class Foo { static ['prototype']() {} }",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        ("(class { 'a'() {} })", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        ("(class { [x]() {} })", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        (
            "(class { ['constructor']() {} })",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        (
            "(class { static ['prototype']() {} })",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        ("class Foo { 'x'() {} }", None),
        ("(class { [x]() {} })", None),
        ("class Foo { static constructor() {} }", None),
        ("class Foo { prototype() {} }", None),
        (
            "class Foo { ['x']() {} }",
            Some(serde_json::json!([{ "enforceForClassMembers": false }])),
        ),
        ("(class { ['x']() {} })", Some(serde_json::json!([{ "enforceForClassMembers": false }]))),
        (
            "class Foo { static ['constructor']() {} }",
            Some(serde_json::json!([{ "enforceForClassMembers": false }])),
        ),
        (
            "class Foo { ['prototype']() {} }",
            Some(serde_json::json!([{ "enforceForClassMembers": false }])),
        ),
        ("class Foo { a }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        (
            "class Foo { ['constructor'] }",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        (
            "class Foo { static ['constructor'] }",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        (
            "class Foo { static ['prototype'] }",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        ("({ [99999999999999999n]: 0 })", None), // { "ecmaVersion": 2020 }
    ];

    let fail = vec![
        ("({ ['0']: 0 })", None),
        ("var { ['0']: a } = obj", None),
        ("({ ['0+1,234']: 0 })", None),
        ("({ [0]: 0 })", None),
        ("var { [0]: a } = obj", None),
        ("({ ['x']: 0 })", None),
        ("var { ['x']: a } = obj", None),
        ("var { ['__proto__']: a } = obj", None),
        ("({ ['x']() {} })", None),
        ("({ [/* this comment prevents a fix */ 'x']: 0 })", None),
        ("({ ['x' /* this comment also prevents a fix */]: 0 })", None),
        ("({ [('x')]: 0 })", None),
        ("var { [('x')]: a } = obj", None),
        ("({ *['x']() {} })", None),
        ("({ async ['x']() {} })", None), // { "ecmaVersion": 8 },
        ("({ get[.2]() {} })", None),
        ("({ set[.2](value) {} })", None),
        ("({ async[.2]() {} })", None), // { "ecmaVersion": 8 },
        ("({ [2]() {} })", None),
        ("({ get [2]() {} })", None),
        ("({ set [2](value) {} })", None),
        ("({ async [2]() {} })", None), // { "ecmaVersion": 8 },
        ("({ get[2]() {} })", None),
        ("({ set[2](value) {} })", None),
        ("({ async[2]() {} })", None), // { "ecmaVersion": 8 },
        ("({ get['foo']() {} })", None),
        ("({ *[2]() {} })", None),
        ("({ async*[2]() {} })", None),
        ("({ ['constructor']: 1 })", None),
        ("({ ['prototype']: 1 })", None),
        ("class Foo { ['0']() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        ("class Foo { ['0+1,234']() {} }", Some(serde_json::json!([{}]))),
        ("class Foo { ['x']() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
        ("class Foo { [/* this comment prevents a fix */ 'x']() {} }", None),
        ("class Foo { ['x' /* this comment also prevents a fix */]() {} }", None),
        ("class Foo { [('x')]() {} }", None),
        ("class Foo { *['x']() {} }", None),
        ("class Foo { async ['x']() {} }", None), // { "ecmaVersion": 8 },
        ("class Foo { get[.2]() {} }", None),
        ("class Foo { set[.2](value) {} }", None),
        ("class Foo { async[.2]() {} }", None), // { "ecmaVersion": 8 },
        ("class Foo { [2]() {} }", None),
        ("class Foo { get [2]() {} }", None),
        ("class Foo { set [2](value) {} }", None),
        ("class Foo { async [2]() {} }", None), // { "ecmaVersion": 8 },
        ("class Foo { get[2]() {} }", None),
        ("class Foo { set[2](value) {} }", None),
        ("class Foo { async[2]() {} }", None), // { "ecmaVersion": 8 },
        ("class Foo { get['foo']() {} }", None),
        ("class Foo { *[2]() {} }", None),
        ("class Foo { async*[2]() {} }", None),
        ("class Foo { static ['constructor']() {} }", None),
        ("class Foo { ['prototype']() {} }", None),
        ("(class { ['x']() {} })", None),
        ("(class { ['__proto__']() {} })", None),
        ("(class { static ['__proto__']() {} })", None),
        ("(class { static ['constructor']() {} })", None),
        ("(class { ['prototype']() {} })", None),
        ("class Foo { ['0'] }", None),
        ("class Foo { ['0'] = 0 }", None),
        ("class Foo { static[0] }", None),
        ("class Foo { ['#foo'] }", None),
        ("(class { ['__proto__'] })", None),
        ("(class { static ['__proto__'] })", None),
        ("(class { ['prototype'] })", None),
    ];

    let fix = vec![
        ("({ ['0']: 0 })", "({ '0': 0 })", None),
        ("var { ['0']: a } = obj", "var { '0': a } = obj", None),
        ("({ ['0+1,234']: 0 })", "({ '0+1,234': 0 })", None),
        ("({ [0]: 0 })", "({ 0: 0 })", None),
        ("var { [0]: a } = obj", "var { 0: a } = obj", None),
        ("({ ['x']: 0 })", "({ 'x': 0 })", None),
        ("var { ['x']: a } = obj", "var { 'x': a } = obj", None),
        ("var { ['__proto__']: a } = obj", "var { '__proto__': a } = obj", None),
        ("({ ['x']() {} })", "({ 'x'() {} })", None),
        ("({ [('x')]: 0 })", "({ 'x': 0 })", None),
        ("var { [('x')]: a } = obj", "var { 'x': a } = obj", None),
        ("({ *['x']() {} })", "({ *'x'() {} })", None),
        ("({ async ['x']() {} })", "({ async 'x'() {} })", None),
        ("({ get[.2]() {} })", "({ get.2() {} })", None),
        ("({ set[.2](value) {} })", "({ set.2(value) {} })", None),
        ("({ async[.2]() {} })", "({ async.2() {} })", None),
        ("({ [2]() {} })", "({ 2() {} })", None),
        ("({ get [2]() {} })", "({ get 2() {} })", None),
        ("({ set [2](value) {} })", "({ set 2(value) {} })", None),
        ("({ async [2]() {} })", "({ async 2() {} })", None),
        ("({ get[2]() {} })", "({ get 2() {} })", None),
        ("({ set[2](value) {} })", "({ set 2(value) {} })", None),
        ("({ async[2]() {} })", "({ async 2() {} })", None),
        ("({ get['foo']() {} })", "({ get'foo'() {} })", None),
        ("({ *[2]() {} })", "({ *2() {} })", None),
        ("({ async*[2]() {} })", "({ async*2() {} })", None),
        ("({ ['constructor']: 1 })", "({ 'constructor': 1 })", None),
        ("({ ['prototype']: 1 })", "({ 'prototype': 1 })", None),
        (
            "class Foo { ['0']() {} }",
            "class Foo { '0'() {} }",
            Some(serde_json::json!([{ "enforceForClassMembers": true }])),
        ),
        (
            "class Foo { ['0+1,234']() {} }",
            "class Foo { '0+1,234'() {} }",
            Some(serde_json::json!([{}])),
        ),
        // (
        //     "class Foo { ['x']() {} }",
        //     "class Foo { 'x'() {} }",
        //     Some(serde_json::json!([{ "enforceForClassMembers": void 0 }])), // there is something ironic about this one causing a syntax error.
        // ),
        ("class Foo { [('x')]() {} }", "class Foo { 'x'() {} }", None),
        ("class Foo { *['x']() {} }", "class Foo { *'x'() {} }", None),
        ("class Foo { async ['x']() {} }", "class Foo { async 'x'() {} }", None),
        ("class Foo { get[.2]() {} }", "class Foo { get.2() {} }", None),
        ("class Foo { set[.2](value) {} }", "class Foo { set.2(value) {} }", None),
        ("class Foo { async[.2]() {} }", "class Foo { async.2() {} }", None),
        ("class Foo { [2]() {} }", "class Foo { 2() {} }", None),
        ("class Foo { get [2]() {} }", "class Foo { get 2() {} }", None),
        ("class Foo { set [2](value) {} }", "class Foo { set 2(value) {} }", None),
        ("class Foo { async [2]() {} }", "class Foo { async 2() {} }", None),
        ("class Foo { get[2]() {} }", "class Foo { get 2() {} }", None),
        ("class Foo { set[2](value) {} }", "class Foo { set 2(value) {} }", None),
        ("class Foo { async[2]() {} }", "class Foo { async 2() {} }", None),
        ("class Foo { get['foo']() {} }", "class Foo { get'foo'() {} }", None),
        ("class Foo { *[2]() {} }", "class Foo { *2() {} }", None),
        ("class Foo { async*[2]() {} }", "class Foo { async*2() {} }", None),
        (
            "class Foo { static ['constructor']() {} }",
            "class Foo { static 'constructor'() {} }",
            None,
        ),
        ("class Foo { ['prototype']() {} }", "class Foo { 'prototype'() {} }", None),
        ("(class { ['x']() {} })", "(class { 'x'() {} })", None),
        ("(class { ['__proto__']() {} })", "(class { '__proto__'() {} })", None),
        ("(class { static ['__proto__']() {} })", "(class { static '__proto__'() {} })", None),
        ("(class { static ['constructor']() {} })", "(class { static 'constructor'() {} })", None),
        ("(class { ['prototype']() {} })", "(class { 'prototype'() {} })", None),
        ("class Foo { ['0'] }", "class Foo { '0' }", None),
        ("class Foo { ['0'] = 0 }", "class Foo { '0' = 0 }", None),
        ("class Foo { static[0] }", "class Foo { static 0 }", None),
        ("class Foo { ['#foo'] }", "class Foo { '#foo' }", None),
        ("(class { ['__proto__'] })", "(class { '__proto__' })", None),
        ("(class { static ['__proto__'] })", "(class { static '__proto__' })", None),
        ("(class { ['prototype'] })", "(class { 'prototype' })", None),
    ];

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