use lazy_regex::Regex;
use oxc_span::Span;
use rustc_hash::FxHashSet;
use schemars::JsonSchema;
use serde::Deserialize;

use oxc_ast::{
    AstKind,
    ast::{FormalParameters, MethodDefinitionKind, TSType},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_semantic::{AstNode, JSDoc, NodeId};

use crate::{
    context::LintContext,
    rule::Rule,
    utils::{
        ParamKind, collect_params, deserialize_regex, get_function_nearest_jsdoc_node,
        should_ignore_as_avoid, should_ignore_as_internal, should_ignore_as_private,
    },
};

fn require_param_diagnostic(violations: Vec<Span>) -> OxcDiagnostic {
    OxcDiagnostic::warn("Missing JSDoc `@param` declaration for function parameters.")
        .with_help("Add `@param` tag with name.")
        .with_labels(violations)
}

#[derive(Debug, Default, Clone)]
pub struct RequireParam(Box<RequireParamConfig>);

#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
struct RequireParamConfig {
    /// List of JSDoc tags that exempt functions from `@param` checking.
    #[serde(default = "default_exempted_by")]
    exempted_by: Vec<String>,
    /// Whether to check constructor methods.
    check_constructors: bool,
    /// Whether to check getter methods.
    check_getters: bool,
    /// Whether to check setter methods.
    check_setters: bool,
    /// Whether to check destructured parameters when you have code like
    /// `function doSomething({ a, b }) { ... }`. Because there is no named
    /// parameter in this example, when this option is `true` you must
    /// have a `@param` tag that corresponds to `{a, b}`.
    check_destructured_roots: bool,
    /// Whether to check destructured parameters.
    check_destructured: bool,
    /// Whether to check rest properties.
    check_rest_property: bool,
    /// Regex pattern to match types that exempt parameters from checking.
    #[serde(default = "default_check_types_pattern", deserialize_with = "deserialize_regex")]
    #[schemars(with = "String", default = "default_check_types_pattern_str")]
    check_types_pattern: Regex,
    /// Set to `true` if you wish to expect documentation of properties on objects supplied as default values. Defaults to `false`.
    use_default_object_properties: bool,
    /// Set to `true` to ignore reporting when all params are missing. Defaults to `false`.
    ignore_when_all_params_missing: bool,
    /// Set if you wish TypeScript interfaces to exempt checks for the existence of `@param`'s.
    /// Will check for a type defining the function itself (on a variable declaration) or if there is a single destructured object with a type. Defaults to `false`.
    interface_exempts_params_check: bool,
}

impl Default for RequireParamConfig {
    fn default() -> Self {
        Self {
            exempted_by: default_exempted_by(),
            check_constructors: false,
            check_getters: true,
            check_setters: true,
            check_destructured_roots: true,
            check_destructured: true,
            check_rest_property: false,
            check_types_pattern: default_check_types_pattern(),
            ignore_when_all_params_missing: false,
            interface_exempts_params_check: false,
            use_default_object_properties: false,
        }
    }
}

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Requires that all function parameters are documented with JSDoc `@param` tags.
    ///
    /// ### Why is this bad?
    ///
    /// The rule is aimed at enforcing code quality and maintainability by requiring
    /// that all function parameters are documented.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```javascript
    /// /** @param foo */
    /// function quux (foo, bar) {}
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```javascript
    /// /** @param foo */
    /// function quux (foo) {}
    /// ```
    RequireParam,
    jsdoc,
    pedantic,
    pending,
    config = RequireParamConfig,
    version = "0.4.3",
    short_description = "Requires that all function parameters are documented with JSDoc `@param` tags.",
);

impl Rule for RequireParam {
    fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        let Some(value) = value.as_array().and_then(|arr| arr.first()) else {
            return Ok(Self::default());
        };
        serde_json::from_value(value.clone()).map(|config| Self(Box::new(config)))
    }

    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        // Collected targets from `FormalParameters`
        let params_to_check = match node.kind() {
            AstKind::Function(func) if !func.is_typescript_syntax() => {
                if self.0.interface_exempts_params_check
                    && (is_parent_fn_typed(func.node_id(), ctx)
                        || is_first_fn_param_typed(&func.params))
                {
                    return;
                }

                collect_params(&func.params, self.0.use_default_object_properties)
            }
            AstKind::ArrowFunctionExpression(arrow_func) => {
                if self.0.interface_exempts_params_check
                    && (is_parent_fn_typed(arrow_func.node_id(), ctx)
                        || is_first_fn_param_typed(&arrow_func.params))
                {
                    return;
                }

                collect_params(&arrow_func.params, self.0.use_default_object_properties)
            }
            // If not a function, skip
            _ => return,
        };

        let config = &self.0;

        let Some(func_def_node) = get_function_nearest_jsdoc_node(node, ctx) else {
            return;
        };
        // If no JSDoc is found, skip
        let Some(jsdocs) = ctx.jsdoc().get_all_by_node(ctx.nodes(), func_def_node) else {
            return;
        };

        let settings = &ctx.settings().jsdoc;

        // If config disabled checking, skip
        if let AstKind::MethodDefinition(method_def) = func_def_node.kind() {
            match method_def.kind {
                MethodDefinitionKind::Get => {
                    if !config.check_getters {
                        return;
                    }
                }
                MethodDefinitionKind::Set => {
                    if !config.check_setters {
                        return;
                    }
                }
                MethodDefinitionKind::Constructor => {
                    if !config.check_constructors {
                        return;
                    }
                }
                MethodDefinitionKind::Method => {}
            }
        }

        // If there is a `@type` tag for this function, then `@param` is not required
        if jsdocs.iter().any(has_type_tag) {
            return;
        }

        // If JSDoc is found but safely ignored, skip
        if jsdocs
            .iter()
            .filter(|jsdoc| !should_ignore_as_avoid(jsdoc, settings, &config.exempted_by))
            .filter(|jsdoc| !should_ignore_as_private(jsdoc, settings))
            .filter(|jsdoc| !should_ignore_as_internal(jsdoc, settings))
            .count()
            == 0
        {
            return;
        }

        // Collected JSDoc `@param` tags
        let tags_to_check = collect_tags(&jsdocs, settings.resolve_tag_name("param"));

        if config.ignore_when_all_params_missing && tags_to_check.is_empty() {
            return;
        }

        let shallow_tags =
            tags_to_check.iter().filter(|(name, _)| !name.contains('.')).collect::<Vec<_>>();

        let check_types_regex = &config.check_types_pattern;

        let mut violations = vec![];

        for (idx, param) in params_to_check.iter().enumerate() {
            match param {
                ParamKind::Single(param) => {
                    if !config.check_rest_property && param.is_rest {
                        continue;
                    }

                    if !tags_to_check.iter().any(|(name, _)| **name == param.name) {
                        violations.push(param.span);
                    }
                }
                ParamKind::Nested(params) => {
                    // If false, skip nested root
                    if !config.check_destructured_roots {
                        continue;
                    }

                    let matched_param_tag = shallow_tags.get(idx);

                    // If {type} exists...
                    if let Some((_, Some(r#type))) = matched_param_tag {
                        // ... and doesn't match the pattern, skip
                        if !check_types_regex.is_match(r#type) {
                            continue;
                        }
                    }

                    // If false, skip nested props
                    if !config.check_destructured {
                        continue;
                    }

                    let root_name = matched_param_tag.map_or("", |(name, _)| name);
                    let mut not_checking_names = FxHashSet::default();
                    for param in params {
                        if !config.check_rest_property && param.is_rest {
                            continue;
                        }

                        let full_param_name = format!("{root_name}.{}", param.name);
                        for (name, type_part) in &tags_to_check {
                            if !is_name_equal(name, &full_param_name) {
                                continue;
                            }
                            let Some(r#type) = type_part else {
                                continue;
                            };
                            if check_types_regex.is_match(r#type) {
                                continue;
                            }

                            not_checking_names.insert(name);
                        }

                        if not_checking_names.iter().any(|&name| full_param_name.starts_with(name))
                        {
                            continue;
                        }

                        if !tags_to_check
                            .iter()
                            .any(|(name, _)| is_name_equal(name, &full_param_name))
                        {
                            violations.push(param.span);
                        }
                    }
                }
            }
        }

        if !violations.is_empty() {
            ctx.diagnostic(require_param_diagnostic(violations));
        }
    }
}

fn default_exempted_by() -> Vec<String> {
    vec!["inheritdoc".to_string()]
}

const DEFAULT_CHECK_TYPES_PATTERN: &str =
    "^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$"; // spellchecker:disable-line

fn default_check_types_pattern_str() -> String {
    DEFAULT_CHECK_TYPES_PATTERN.to_string()
}

fn default_check_types_pattern() -> Regex {
    Regex::new(DEFAULT_CHECK_TYPES_PATTERN)
        .expect("default `checkTypesPattern` is a valid regex pattern")
}

fn collect_tags<'a>(
    jsdocs: &[JSDoc<'a>],
    resolved_param_tag_name: &str,
) -> Vec<(&'a str, Option<&'a str>)> {
    let mut collected = vec![];

    for tag in jsdocs
        .iter()
        .flat_map(JSDoc::tags)
        .filter(|tag| tag.kind.parsed() == resolved_param_tag_name)
    {
        let (type_part, Some(name_part), _) = tag.type_name_comment() else {
            continue;
        };

        let name = name_part.parsed();
        // thisParam is special, not collected as `FormalParameter`, should be ignored
        if name == "this" {
            continue;
        }

        collected.push((name, type_part.map(|p| p.parsed())));
    }

    collected
}

/// Returns true if the JSDoc has a `@type` tag in it
fn has_type_tag(jsdoc: &JSDoc) -> bool {
    jsdoc.tags().iter().any(|tag| "type" == tag.kind.parsed())
}

/// Compare to string param names without quotes
/// e.g. `foo."bar"`
fn is_name_equal(a: &str, b: &str) -> bool {
    let mut a_chars = a.chars().filter(|&c| c != '"');
    let mut b_chars = b.chars().filter(|&c| c != '"');

    loop {
        match (a_chars.next(), b_chars.next()) {
            (Some(ac), Some(bc)) if ac == bc => {}
            (None, None) => return true, // Both done
            _ => return false,           // Either one is done, or not equal
        }
    }
}

fn is_first_fn_param_typed(params: &FormalParameters) -> bool {
    if let Some(param) = &params.items.first()
        && let Some(param_type) = &param.type_annotation
        && matches!(param_type.type_annotation, TSType::TSTypeReference(_))
    {
        return true;
    }

    false
}

fn is_parent_fn_typed(node_id: NodeId, ctx: &LintContext) -> bool {
    if matches!(
        ctx.nodes().parent_node(node_id).kind(),
        AstKind::VariableDeclarator(var) if var.type_annotation.is_some()
    ) {
        return true;
    }

    false
}

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

    let pass = vec![
        ("
			          /**
			           * @param foo
			           */
			          function quux (foo) {

			          }
			      ", None, None),
        (
            "
                      /**
                       * @param md
                       */
                      const component = (md) => {
                        md.renderer.rules.fence = (...args) => {
                          const [tokens, index] = args;
                          return tokens[index];
                        };
                      };
                  ",
            None,
            None,
        ),
        (
            "
                      /**
                       * Random float in [min, max).
                       * @param {number} min - Minimum float value.
                       * @param {number} max - Maximum float value.
                       * @returns {number} Random float in [min, max).
                       */
                      function randomRange(min, max) {
                        return min + Math.random() * (max - min);
                      }
                  ",
            None,
            None,
        ),

("
			          /**
			           * @param root0
			           * @param root0.foo
			           */
			          function quux ({foo}) {

			          }
			      ", None, None),
("
			          /**
			           * @param root0
			           * @param root0.foo
			           * @param root1
			           * @param root1.bar
			           */
			          function quux ({foo}, {bar}) {

			          }
			      ", None, None),
// Disabled: these cases configure `unnamedRootBase`, an `eslint-plugin-jsdoc` option this rule
// does not implement. `deny_unknown_fields` rejects it, so the config no longer deserializes.
// ("
// 			          /**
// 			           * @param arg0
// 			           * @param arg0.foo
// 			           * @param arg1
// 			           * @param arg1.bar
// 			           */
// 			          function quux ({foo}, {bar}) {
//
// 			          }
// 			      ", Some(serde_json::json!([{ "unnamedRootBase": ["arg"] }])), None),
// ("
// 			          /**
// 			           * @param arg
// 			           * @param arg.foo
// 			           * @param config0
// 			           * @param config0.bar
// 			           * @param config1
// 			           * @param config1.baz
// 			           */
// 			          function quux ({foo}, {bar}, {baz}) {
//
// 			          }
// 			      ", Some(serde_json::json!([{ "unnamedRootBase": ["arg", "config"] }])), None),
("
			          /**
			           * @inheritdoc
			           */
			          function quux (foo) {

			          }
			      ", None, None),
("
			          /**
			           * @arg foo
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "tagNamePreference": { "param": "arg" } } } }))),
("
			          /**
			           * @override
			           * @param foo
			           */
			          function quux (foo) {

			          }
			      ", None, None),
("
			          /**
			           * @override
			           */
			          function quux (foo) {

			          }
			      ", None, None),
("
			          /**
			           * @override
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "overrideReplacesDocs": true } } }))),
("
			          /**
			           * @ignore
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "ignoreReplacesDocs": true } } }))),
("
			          /**
			           * @implements
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "implementsReplacesDocs": true } } }))),
("
			          /**
			           * @implements
			           * @param foo
			           */
			          function quux (foo) {

			          }
			      ", None, None),
("
			          /**
			           * @augments
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "augmentsExtendsReplacesDocs": true } } }))),
("
			          /**
			           * @augments
			           * @param foo
			           */
			          function quux (foo) {

			          }
			      ", None, None),
("
			          /**
			           * @extends
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "augmentsExtendsReplacesDocs": true } } }))),
("
			          /**
			           * @extends
			           * @param foo
			           */
			          function quux (foo) {

			          }
			      ", None, None),
("
			          /**
			           * @internal
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "ignoreInternal": true } } }))),
("
			          /**
			           * @private
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "ignorePrivate": true } } }))),
("
			          /**
			           * @access private
			           */
			          function quux (foo) {

			          }
			      ", None, Some(serde_json::json!({ "settings": { "jsdoc": { "ignorePrivate": true } } }))),
("
			          // issue 182: optional chaining
			          /** @const {boolean} test */
			          const test = something?.find(_ => _)
			      ", None, None), // {        "parser": babelEslintParser,      },
("
			          /**
			           * @type {MyCallback}
			           */
			          function quux () {

			          }
			      ", Some(serde_json::json!([{ "exemptedBy": ["type"] }])), None),
("
			        export class SomeClass {
			          /**
			           * @param property
			           */
			          constructor(private property: string) {}
			        }
			      ", None, None), // {        "parser": typescriptEslintParser,        "sourceType": "module",      },
("
			      /**
			       * Assign the project to an employee.
			       *
			       * @param {object} employee - The employee who is responsible for the project.
			       * @param {string} employee.name - The name of the employee.
			       * @param {string} employee.department - The employee's department.
			       */
			      function assign({name, department}) {
			        // ...
			      }
			      ", None, None),
("
			    export abstract class StephanPlugin<O, D> {

			        /**
			         * Called right after Stephan loads the plugin file.
			         *
			         * @example
			         *```typescript
			         * type Options = {
			         *      verbose?: boolean;
			         *      token?: string;
			         * }
			         * ```
			         *
			         * Note that your Options type should only have optional properties...
			         *
			         * @param args Arguments compiled and provided by StephanClient.
			         * @param args.options The options as provided by the user, or an empty object if not provided.
			         * @param args.client The options as provided by the user, or an empty object if not provided.
			         * @param defaultOptions The default options as provided by the plugin, or an empty object.
			         */
			        public constructor({options, client}: {
			            options: O;
			            client: unknown;
			        }, defaultOptions: D) {

			        }
			    }
			      ", None, None), // {        "parser": typescriptEslintParser      },
("
			        export abstract class StephanPlugin<O, D> {

			        /**
			         * Called right after Stephan loads the plugin file.
			         *
			         * @example
			         *```typescript
			         * type Options = {
			         *      verbose?: boolean;
			         *      token?: string;
			         * }
			         * ```
			         *
			         * Note that your Options type should only have optional properties...
			         *
			         * @param args Arguments compiled and provided by StephanClient.
			         * @param args.options The options as provided by the user, or an empty object if not provided.
			         * @param args.client The options as provided by the user, or an empty object if not provided.
			         * @param args.client.name The name of the client.
			         * @param defaultOptions The default options as provided by the plugin, or an empty object.
			         */
			        public constructor({ options, client: { name } }: {
			            options: O;
			            client: { name: string };
			        }, defaultOptions: D) {

			        }
			    }
			      ", None, None), // {        "parser": typescriptEslintParser      },
("
			      /**
			      * @param {string} cb
			      */
			      function createGetter (cb) {
			        return function (...args) {
			          cb();
			        };
			      }
			      ", None, None),
("
			      /**
			       * @param cfg
			       * @param cfg.num
			       */
			      function quux ({num, ...extra}) {
			      }
			      ", None, None),
("
			      /**
			      * Converts an SVGRect into an object.
			      * @param {SVGRect} bbox - a SVGRect
			      */
			      const bboxToObj = function ({x, y, width, height}) {
			        return {x, y, width, height};
			      };
			      ", None, None),
("
			      /**
			      * Converts an SVGRect into an object.
			      * @param {object} bbox - a SVGRect
			      */
			      const bboxToObj = function ({x, y, width, height}) {
			        return {x, y, width, height};
			      };
			      ", Some(serde_json::json!([{ "checkTypesPattern": "SVGRect" }])), None),
("
			      class CSS {
			        /**
			         * Set one or more CSS properties for the set of matched elements.
			         *
			         * @param {Object} propertyObject - An object of property-value pairs to set.
			         */
			        setCssObject(propertyObject: {[key: string]: string | number}): void {
			        }
			      }
			      ", None, None), // {        "parser": typescriptEslintParser      },
("
			          /**
			           * @param foo
			           * @param bar
			           * @param cfg
			           */
			          function quux (foo, bar, {baz}) {

			          }
			      ", Some(serde_json::json!([{ "checkDestructured": false }])), None),
("
			          /**
			           * @param foo
			           * @param bar
			           */
			          function quux (foo, bar, {baz}) {

			          }
			      ", Some(serde_json::json!([{ "checkDestructuredRoots": false }])), None),
(r#"
			          /**
			           * @param root
			           * @param root.foo
			           */
			          function quux ({"foo": bar}) {

			          }
			      "#, None, None),
(r#"
			          /**
			           * @param root
			           * @param root."foo"
			           */
			          function quux ({foo: bar}) {

			          }
			      "#, None, None),
("
			      /**
			       * Description.
			       * @param {string} b Description `/**`.
			       */
			      module.exports = function a(b) {
			        console.info(b);
			      };
			      ", None, None),
("
			      /**
			       * Description.
			       * @param {Object} options Options.
			       * @param {FooBar} options.foo foo description.
			       */
			      function quux ({ foo: { bar } }) {}
			      ", None, None),
("
			      /**
			       * Description.
			       * @param {FooBar} options
			       * @param {Object} options.foo
			       */
			      function quux ({ foo: { bar } }) {}
			      ", Some(serde_json::json!([{ "checkTypesPattern": "FooBar" }])), None),
(r#"
			      /**
			       * @param obj
			       * @param obj.data
			       * @param obj.data."0"
			       * @param obj.data."1"
			       * @param obj.data."2"
			       * @param obj.defaulting
			       * @param obj.defaulting."0"
			       * @param obj.defaulting."1"
			       */
			      function Item({
			        data: [foo, bar, ...baz],
			        defaulting: [quux, xyz] = []
			      }) {
			      }
			      "#, None, None),
("
			      /**
			      * Returns a number.
			      * @param {Object} props Props.
			      * @param {Object} props.prop Prop.
			      * @return {number} A number.
			      */
			      export function testFn1 ({ prop = { a: 1, b: 2 } }) {
			      }
			      ", Some(serde_json::json!([{ "useDefaultObjectProperties": false }])), None), // {        "sourceType": "module",      },
("
			      /**
			       * @param this The this object
			       * @param bar number to return
			       * @returns number returned back to caller
			       */
			      function foo(this: T, bar: number): number {
			        console.log(this.name);
			        return bar;
			      }
			      ", None, None), // {        "parser": typescriptEslintParser      },
("
			      /**
			       * @param bar number to return
			       * @returns number returned back to caller
			       */
			      function foo(this: T, bar: number): number {
			        console.log(this.name);
			        return bar;
			      }
			      ", None, None), // {        "parser": typescriptEslintParser      },
("
			        /**
			         * Returns the sum of two numbers
			         * @param options Object to destructure
			         * @param options.a First value
			         * @param options.b Second value
			         * @returns Sum of a and b
			         */
			        function sumDestructure(this: unknown, { a, b }: { a: number, b: number }) {
			          return a + b;
			        }
			      ", None, None), // {        "parser": typescriptEslintParser,      }
        // https://github.com/oxc-project/oxc/issues/10253
        ("
            /** @typedef {import('../types.d.ts').FileURL} FileURL */

            /**
             * @type {import('node:module').ResolveHook}
             */
            async function resolveJSONC(specifier, ctx, nextResolve) {}
        ", None, None),
        (
            r#"
			      /**
			       * A shiki transformer.
			       */
			      const shikiTransformer: ShikiTransformer = {
			        name: "example",
			        tokens(tokens) {
			        }
			      };
			      "#,
            None,
            None,
        ),
        (
            "
				          /**
				           * Some desc.
				           */
				          function quux (a, b) {}
				      ",
            Some(
                serde_json::json!([{ "ignoreWhenAllParamsMissing": true }]),
            ),
            None,
        ),
        (
            "
				          /**
				           *
				           */
				          const quux: FunctionInterface = function quux (foo) {
				          };
				      ",
            Some(
                serde_json::json!([{ "interfaceExemptsParamsCheck": true }]),
            ),
            None,
        ),
        (
            "
				          /**
				           *
				           */
				          function quux ({
				            abc,
				            def
				          }: FunctionInterface) {
				          }
				      ",
            Some(
                serde_json::json!([{ "interfaceExemptsParamsCheck": true }]),
            ),
            None,
        ),
        (
            "
				          /**
				           *
				           */
				          export async function fetchMarketstackEOD(
				            parameters: FetchEODParameters,
				          ): Promise<MarketstackDataPoint[]>
				          {
				            // ...
				          };
				      ",
            Some(
                serde_json::json!([{ "interfaceExemptsParamsCheck": true }]),
            ),
            None,
        ),
    ];

    let fail = vec![
        (
            "
			          /**
			           *
			           */
			          function quux (foo) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           *
			           */
			          function quux ({foo}) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param foo
			           */
			          function quux (foo, bar, {baz}) {

			          }
			      ",
            Some(serde_json::json!([{ "checkDestructured": false }])),
            None,
        ),
        (
            "
			          /**
			           * @param foo
			           */
			          function quux (foo, bar, {baz}) {

			          }
			      ",
            Some(serde_json::json!([{ "checkDestructuredRoots": false }])),
            None,
        ),
        // Disabled: this case configures an `eslint-plugin-jsdoc` option this rule does not
        // implement, which `deny_unknown_fields` rejects.
        // (
        // "
        // /**
        // *
        // */
        // function quux ({foo}) {
        //
        // }
        // ",
        // Some(serde_json::json!([{ "enableFixer": false }])),
        // None,
        // ),
        (
            "
			          /**
			           *
			           */
			          function quux ({foo: bar = 5} = {}) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param
			           */
			          function quux ({foo}) {

			          }
			      ",
            None,
            None,
        ),
        // Disabled: this case configures an `eslint-plugin-jsdoc` option this rule does not
        // implement, which `deny_unknown_fields` rejects.
        // (
        // "
        // /**
        // * @param
        // */
        // function quux ({foo}) {
        //
        // }
        // ",
        // Some(serde_json::json!([{ "autoIncrementBase": 1 }])),
        // None,
        // ),
        (
            "
			          /**
			           * @param options
			           */
			          function quux ({foo}) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param
			           */
			          function quux ({ foo, bar: { baz }}) {

			          }
			      ",
            None,
            None,
        ),
        // Disabled: this case configures an `eslint-plugin-jsdoc` option this rule does not
        // implement, which `deny_unknown_fields` rejects.
        // (
        // "
        // /**
        // *
        // */
        // function quux ({foo}, {bar}) {
        //
        // }
        // ",
        // Some(
        // serde_json::json!([{ "unnamedRootBase": ["arg"] }]),
        // ),
        // None,
        // ),
        // Disabled: this case configures an `eslint-plugin-jsdoc` option this rule does not
        // implement, which `deny_unknown_fields` rejects.
        // (
        // "
        // /**
        // *
        // */
        // function quux ({foo}, {bar}) {
        //
        // }
        // ",
        // Some(
        // serde_json::json!([{ "unnamedRootBase": ["arg", "config"] }]),
        // ),
        // None,
        // ),
        // Disabled: this case configures an `eslint-plugin-jsdoc` option this rule does not
        // implement, which `deny_unknown_fields` rejects.
        // (
        // "
        // /**
        // *
        // */
        // function quux ({foo}, {bar}) {
        //
        // }
        // ",
        // Some(
        // serde_json::json!([{ "enableRootFixer": false, "unnamedRootBase": ["arg", "config"] }]),
        // ),
        // None,
        // ),
        (
            "
			          /**
			           *
			           */
			          function quux (foo, bar) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param foo
			           */
			          function quux (foo, bar) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param bar
			           */
			          function quux (foo, bar, baz) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param foo
			           * @param bar
			           */
			          function quux (foo, bar, baz) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param baz
			           */
			          function quux (foo, bar, baz) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param
			           */
			          function quux (foo) {

			          }
			      ",
            None,
            Some(
                serde_json::json!({ "settings": { "jsdoc": { "tagNamePreference": { "param": "arg" } } } }),
            ),
        ),
        (
            "
			          /**
			           * @override
			           */
			          function quux (foo) {

			          }
			      ",
            None,
            Some(serde_json::json!({ "settings": { "jsdoc": { "overrideReplacesDocs": false } } })),
        ),
        (
            "
			          /**
			           * @ignore
			           */
			          function quux (foo) {

			          }
			      ",
            None,
            Some(serde_json::json!({ "settings": { "jsdoc": { "ignoreReplacesDocs": false } } })),
        ),
        (
            "
			          /**
			           * @implements
			           */
			          function quux (foo) {

			          }
			      ",
            None,
            Some(
                serde_json::json!({ "settings": { "jsdoc": { "implementsReplacesDocs": false } } }),
            ),
        ),
        (
            "
			          /**
			           * @augments
			           */
			          function quux (foo) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @extends
			           */
			          function quux (foo) {

			          }
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			       *
			       */
			      function quux ({bar, baz}, foo) {
			      }
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			       *
			       */
			      function quux (foo, {bar, baz}) {
			      }
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			       *
			       */
			      function quux ([bar, baz], foo) {
			      }
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           *
			           */
			          function quux (foo) {
			          }
			      ",
            Some(serde_json::json!([{ "exemptedBy": ["notPresent"] }])),
            None,
        ),
        (
            "
			          /**
			           * @inheritdoc
			           */
			          function quux (foo) {

			          }
			      ",
            Some(serde_json::json!([{ "exemptedBy": [] }])),
            None,
        ),
        (
            "
			          /**
			           * Assign the project to a list of employees.
			           * @param {object[]} employees - The employees who are responsible for the project.
			           * @param {string} employees[].name - The name of an employee.
			           * @param {string} employees[].department - The employee's department.
			           */
			          function assign (employees, name) {

			          };
			      ",
            None,
            None,
        ),
        (
            "
			          /**
			           * @param baz
			           * @param options
			           */
			          function quux (baz, {foo: bar}) {

			          }
			      ",
            None,
            None,
        ),
        // Disabled: this case configures an `eslint-plugin-jsdoc` option this rule does not
        // implement, which `deny_unknown_fields` rejects.
        // (
        // "
        // /**
        // *
        // */
        // function quux (foo) {
        //
        // }
        // ",
        // Some(serde_json::json!([{ "enableFixer": false }])),
        // None,
        // ),
        (
            "
			      class Client {
			        /**
			         * Set collection data.
			         * @return {Promise<Object, Error>}
			         */
			        async setData(
			          data: { last_modified?: number }
			        ) {}
			      }
			      ",
            None,
            None,
        ), // {        "parser": typescriptEslintParser      },
        (
            "
			      /**
			       * @param cfg
			       * @param cfg.num
			       */
			      function quux ({num, ...extra}) {
			      }
			      ",
            Some(serde_json::json!([{ "checkRestProperty": true }])),
            None,
        ),
        (
            "
			      /**
			       * @param cfg
			       * @param cfg.opts
			       * @param cfg.opts.num
			       */
			      function quux ({opts: {num, ...extra}}) {
			      }
			      ",
            Some(serde_json::json!([{ "checkRestProperty": true }])),
            None,
        ),
        (
            r#"
			      /**
			       * @param {GenericArray} cfg
			       * @param {number} cfg."0"
			       */
			      function baar ([a, ...extra]) {
			        //
			      }
			      "#,
            Some(serde_json::json!([{ "checkRestProperty": true }])),
            None,
        ),
        (
            "
			      /**
			       * @param a
			       */
			      function baar (a, ...extra) {
			        //
			      }
			      ",
            Some(serde_json::json!([{ "checkRestProperty": true }])),
            None,
        ),
        (
            "
			      /**
			       * Converts an SVGRect into an object.
			       * @param {SVGRect} bbox - a SVGRect
			       */
			      const bboxToObj = function ({x, y, width, height}) {
			        return {x, y, width, height};
			      };
			      ",
            Some(serde_json::json!([{ "checkTypesPattern": "SVGRect" }])),
            None,
        ),
        (
            "
			      /**
			       * Converts an SVGRect into an object.
			       * @param {object} bbox - a SVGRect
			       */
			      const bboxToObj = function ({x, y, width, height}) {
			        return {x, y, width, height};
			      };
			      ",
            None,
            None,
        ),
        (
            "
			      module.exports = class GraphQL {
			        /**
			         * @param fetchOptions
			         * @param cacheKey
			         */
			        fetch = ({ url, ...options }, cacheKey) => {
			        }
			      };
			      ",
            Some(serde_json::json!([{ "checkRestProperty": true }])),
            None,
        ), // {        "parser": babelEslintParser,      },
        (
            "
			(function() {
				/**
				 * A function.
				 */
				function f(param) {
					return !param;
				}
			})();
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			       * Description.
			       * @param {Object} options
			       * @param {Object} options.foo
			       */
			      function quux ({ foo: { bar } }) {}
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			       * Description.
			       * @param {FooBar} options
			       * @param {FooBar} options.foo
			       */
			      function quux ({ foo: { bar } }) {}
			      ",
            Some(serde_json::json!([{ "checkTypesPattern": "FooBar" }])),
            None,
        ),
        (
            "
			      /**
			       * Description.
			       * @param {Object} options
			       * @param {FooBar} foo
			       */
			      function quux ({ foo: { bar } }) {}
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			       * Description.
			       * @param {Object} options
			       * @param options.foo
			       */
			      function quux ({ foo: { bar } }) {}
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			       * Description.
			       * @param {object} options Options.
			       * @param {object} options.foo A description.
			       * @param {object} options.foo.bar
			       */
			      function foo({ foo: { bar: { baz } }}) {}
			      ",
            None,
            None,
        ),
        (
            "
			      /**
			      * Returns a number.
			      * @param {Object} props Props.
			      * @param {Object} props.prop Prop.
			      * @return {number} A number.
			      */
			      export function testFn1 ({ prop = { a: 1, b: 2 } }) {
			      }
			      ",
            Some(serde_json::json!([{ "useDefaultObjectProperties": true }])),
            None,
        ), // {        "sourceType": "module",      },
        (
            "
			        /** Foo. */
			        function foo(a, b, c) {}
			      ",
            None,
            None,
        ),
        // https://github.com/oxc-project/oxc/issues/19139#issuecomment-3875380106
        (
            r#"
			const shikiTransformer: ShikiTransformer = {
				name: "example",
				/**
				 * A shiki transformer.
				 */
			        tokens(tokens) {
			        }
			      };
			      "#,
            None,
            None,
        ),
        (
            "
				          /**
				           * Some desc.
				           * @param a
				           */
				          function quux (a, b) {}
				      ",
            Some(serde_json::json!([{ "ignoreWhenAllParamsMissing": true }])),
            None,
        ),
        (
            "
				          /**
				           *
				           */
				          const quux = function quux (foo) {
				          };
				      ",
            Some(serde_json::json!([{ "interfaceExemptsParamsCheck": true }])),
            None,
        ),
        (
            "
				          /**
				           *
				           */
				          function quux ({
				            abc,
				            def
				          }) {
				          }
				      ",
            Some(serde_json::json!([{ "interfaceExemptsParamsCheck": true }])),
            None,
        ),
    ];

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