use rustc_hash::FxHashSet;

use oxc_ast::{
    AstKind,
    ast::{
        BindingPattern, CallExpression, ExportDefaultDeclaration, ExportDefaultDeclarationKind,
        Expression, ObjectExpression, ObjectPropertyKind, PropertyKey, TSMethodSignatureKind,
        TSSignature, TSType, VariableDeclarator,
    },
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_semantic::NodeId;
use oxc_span::{GetSpan, Span};

use crate::{
    AstNode,
    context::LintContext,
    fixer::{RuleFix, RuleFixer},
    frameworks::FrameworkOptions,
    rule::Rule,
    utils::{find_property, for_each_define_props_type_signature},
};

fn no_required_prop_with_default_diagnostic(span: Span, prop_name: &str) -> OxcDiagnostic {
    let msg = format!("Prop \"{prop_name}\" should be optional.");
    OxcDiagnostic::warn(msg)
        .with_help("Remove the `required: true` option, or drop the `required` key entirely to make this prop optional.")
        .with_label(span)
}

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

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Enforce props with default values to be optional.
    ///
    /// ### Why is this bad?
    ///
    /// If a prop is declared with a default value, whether it is required or not,
    /// we can always skip it in actual use. In that situation, the default value would be applied.
    /// So, a required prop with a default value is essentially the same as an optional prop.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```vue
    /// <script setup lang="ts">
    /// const props = withDefaults(
    ///   defineProps<{
    ///     name: string | number
    ///     age?: number
    ///   }>(),
    ///   {
    ///     name: 'Foo',
    ///   }
    /// );
    /// </script>
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```vue
    /// <script setup lang="ts">
    /// const props = withDefaults(
    ///   defineProps<{
    ///     name?: string | number
    ///     age?: number
    ///   }>(),
    ///   {
    ///     name: 'Foo',
    ///   }
    /// );
    /// </script>
    /// ```
    NoRequiredPropWithDefault,
    vue,
    suspicious,
    suggestion,
    version = "1.17.0",
    short_description = "Enforce props with default values to be optional.",
);

impl Rule for NoRequiredPropWithDefault {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::CallExpression(call_expr) => {
                if ctx.file_extension().is_some_and(|ext| ext == "vue") {
                    Self::run_on_vue(node.id(), call_expr, ctx);
                } else {
                    Self::check_define_component(call_expr, ctx);
                }
            }
            AstKind::ExportDefaultDeclaration(export_default_decl)
                if ctx.file_extension().is_some_and(|ext| ext == "vue")
                    && ctx.frameworks_options() != FrameworkOptions::VueSetup =>
            {
                Self::run_on_composition(export_default_decl, ctx);
            }
            _ => {}
        }
    }
}

impl NoRequiredPropWithDefault {
    fn run_on_vue<'a>(node_id: NodeId, call_expr: &CallExpression<'a>, ctx: &LintContext<'a>) {
        if ctx.frameworks_options() == FrameworkOptions::VueSetup {
            Self::run_on_setup(node_id, call_expr, ctx);
        } else {
            Self::check_define_component(call_expr, ctx);
        }
    }

    fn check_define_component(call_expr: &CallExpression<'_>, ctx: &LintContext<'_>) {
        // only check `defineComponent` method
        // e.g. `let component = defineComponent({ props: { name: { required: true, default: 'a' } } })`
        let Some(ident) = call_expr.callee.get_identifier_reference() else {
            return;
        };
        if ident.name.as_str() == "defineComponent" && call_expr.arguments.len() == 1 {
            let arg = &call_expr.arguments[0];
            let Some(Expression::ObjectExpression(obj)) = arg.as_expression() else {
                return;
            };
            handle_object_expression(ctx, obj);
        }
    }

    fn run_on_setup<'a>(node_id: NodeId, call_expr: &CallExpression<'a>, ctx: &LintContext<'a>) {
        let Some(ident) = call_expr.callee.get_identifier_reference() else {
            return;
        };

        match ident.name.as_str() {
            "defineProps" => {
                if let Some(arge) = call_expr.arguments.first() {
                    let Some(Expression::ObjectExpression(obj)) = arge.as_expression() else {
                        return;
                    };
                    // Here we need to consider the following two examples
                    // 1. const props = defineProps({ name: { required: true, default: 'a' } })
                    // 2. const { name = 'a' } =  defineProps({ name: { required: true } })
                    let key_hash =
                        collect_hash_from_variable_declarator(ctx, node_id).unwrap_or_default();
                    handle_prop_object(ctx, obj, Some(&key_hash));
                }
                if call_expr.arguments.is_empty() {
                    // if `defineProps` is used without arguments, we need to check the type arguments
                    // e.g. `const { name = 'a' } = defineProps<IProp>()`
                    let Some(type_args) = &call_expr.type_arguments else {
                        return;
                    };
                    let Some(first_type_argument) = type_args.params.first() else {
                        return;
                    };
                    if let Some(key_hash) = collect_hash_from_variable_declarator(ctx, node_id) {
                        handle_type_argument(ctx, first_type_argument, &key_hash);
                    }
                }
            }
            "withDefaults" if call_expr.arguments.len() == 2 => {
                let [first_arg, second_arg] = call_expr.arguments.as_slice() else {
                    return;
                };
                if let (Some(first_arg_expr), Some(second_arg_expr)) =
                    (first_arg.as_expression(), second_arg.as_expression())
                {
                    let Expression::ObjectExpression(second_obj_expr) =
                        second_arg_expr.get_inner_expression()
                    else {
                        return;
                    };
                    let Some(key_hash) = collect_hash_from_object_expr(second_obj_expr) else {
                        return;
                    };
                    process_define_props_call(ctx, first_arg_expr, &key_hash);
                }
            }
            _ => {
                Self::check_define_component(call_expr, ctx);
            }
        }
    }

    fn run_on_composition<'a>(
        export_default_decl: &ExportDefaultDeclaration<'a>,
        ctx: &LintContext<'a>,
    ) {
        let ExportDefaultDeclarationKind::ObjectExpression(obj_expr) =
            &export_default_decl.declaration
        else {
            return;
        };
        handle_object_expression(ctx, obj_expr);
    }
}

fn collect_hash_from_object_expr(obj: &ObjectExpression) -> Option<FxHashSet<String>> {
    if obj.properties.is_empty() {
        return None;
    }
    let key_hash = obj
        .properties
        .iter()
        .filter_map(|item| {
            if let ObjectPropertyKind::ObjectProperty(obj_prop) = item
                && let Some(key) = obj_prop.key.static_name()
            {
                Some(key.to_string())
            } else {
                None
            }
        })
        .collect();
    Some(key_hash)
}

fn collect_hash_from_variable_declarator(
    ctx: &LintContext<'_>,
    node_id: NodeId,
) -> Option<FxHashSet<String>> {
    let var_decl = get_first_variable_decl_ancestor(ctx, node_id)?;
    let BindingPattern::ObjectPattern(obj_pattern) = &var_decl.id else {
        return None;
    };
    let key_hash: FxHashSet<String> = obj_pattern
        .properties
        .iter()
        .filter_map(|prop| {
            if matches!(prop.value, BindingPattern::AssignmentPattern(_)) {
                prop.key.static_name()
            } else {
                None
            }
        })
        .map(|key| key.to_string())
        .collect();
    Some(key_hash)
}

fn get_first_variable_decl_ancestor<'a>(
    ctx: &LintContext<'a>,
    node_id: NodeId,
) -> Option<&'a VariableDeclarator<'a>> {
    ctx.nodes().ancestors(node_id).find_map(|ancestor| {
        if let AstKind::VariableDeclarator(var_decl) = ancestor.kind() {
            Some(var_decl)
        } else {
            None
        }
    })
}

fn process_define_props_call<'a>(
    ctx: &LintContext<'a>,
    first_arg_expr: &Expression<'a>,
    key_hash: &FxHashSet<String>,
) {
    let Expression::CallExpression(first_call_expr) = first_arg_expr.get_inner_expression() else {
        return;
    };
    let Expression::Identifier(first_call_ident) = &first_call_expr.callee else {
        return;
    };
    if first_call_ident.name != "defineProps" {
        return;
    }
    let Some(type_arguments) = first_call_expr.type_arguments.as_ref() else {
        return;
    };
    let Some(first_type_argument) = type_arguments.params.first() else {
        return;
    };

    handle_type_argument(ctx, first_type_argument, key_hash);
}

/// Creates a fix that inserts `?` after the property key to make it optional.
fn create_optional_fix(fixer: RuleFixer<'_, '_>, key: &PropertyKey) -> RuleFix {
    let insert_pos = key.span().end;
    fixer.insert_text_after_range(Span::new(insert_pos, insert_pos), "?")
}

fn handle_type_argument<'a>(
    ctx: &LintContext<'a>,
    ts_type: &TSType<'a>,
    key_hash: &FxHashSet<String>,
) {
    for_each_define_props_type_signature(ts_type, ctx, &mut |item| {
        let (key_name, optional, key) = match item {
            TSSignature::TSPropertySignature(prop_sign) => {
                (prop_sign.key.static_name(), prop_sign.optional, &prop_sign.key)
            }
            TSSignature::TSMethodSignature(method_sign)
                if method_sign.kind == TSMethodSignatureKind::Method =>
            {
                (method_sign.key.static_name(), method_sign.optional, &method_sign.key)
            }
            _ => return,
        };
        if let Some(key_name) = key_name
            && !optional
            && key_hash.contains(key_name.as_ref())
        {
            let diagnostic =
                no_required_prop_with_default_diagnostic(item.span(), key_name.as_ref());
            // Check for comments around the key before applying fix
            let fix_span = Span::new(key.span().start, key.span().end + 1);
            if ctx.has_comments_between(fix_span) {
                ctx.diagnostic(diagnostic);
            } else {
                ctx.diagnostic_with_suggestion(diagnostic, |fixer| create_optional_fix(fixer, key));
            }
        }
    });
}

fn handle_object_expression(ctx: &LintContext, obj: &ObjectExpression) {
    let Some(prop_obj) = find_property(obj, "props") else {
        return;
    };
    let Expression::ObjectExpression(prop_obj_expr) = prop_obj.value.get_inner_expression() else {
        return;
    };
    handle_prop_object(ctx, prop_obj_expr, None);
}

fn handle_prop_object(
    ctx: &LintContext,
    obj: &ObjectExpression,
    key_hash: Option<&FxHashSet<String>>,
) {
    obj.properties.iter().for_each(|v| {
        if let ObjectPropertyKind::ObjectProperty(inner_prop) = v
            && let Some(inner_key) = inner_prop.key.static_name()
            && let Expression::ObjectExpression(inner_prop_value_expr) =
                inner_prop.value.get_inner_expression()
        {
            let mut has_default_key = false;
            let mut required_true_span: Option<Span> = None;

            // Sometimes the default value comes from the `ObjectPattern` of a `VariableDeclarator`,
            // e.g. `const { name = 2 } = defineProps()`
            if key_hash.is_some_and(|hash| hash.contains(inner_key.as_ref())) {
                has_default_key = true;
            }

            for property in &inner_prop_value_expr.properties {
                if let ObjectPropertyKind::ObjectProperty(item_obj) = property
                    && let Some(item_key) = item_obj.key.static_name()
                {
                    if item_key == "default" {
                        has_default_key = true;
                    }
                    if item_key == "required" {
                        let Expression::BooleanLiteral(inner_value) = &item_obj.value else {
                            continue;
                        };
                        if inner_value.value {
                            required_true_span = Some(inner_value.span);
                        } else {
                            break;
                        }
                    }

                    if has_default_key && let Some(span) = required_true_span {
                        let diagnostic = no_required_prop_with_default_diagnostic(
                            inner_prop.span(),
                            inner_key.as_ref(),
                        );
                        // Check for comments around the value before applying fix
                        if ctx.has_comments_between(span) {
                            ctx.diagnostic(diagnostic);
                        } else {
                            ctx.diagnostic_with_suggestion(diagnostic, |fixer| {
                                fixer.replace(span, "false")
                            });
                        }
                        break;
                    }
                }
            }
        }
    });
}

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

    let pass = vec![
        (
            r#"
            <script setup lang="ts">
                const { laps } = defineProps<{
                    laps: unknown[];
                }>();
            </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            r#"
                <script setup lang="ts">
                    interface TestPropType {
                    name?: string
                    age?: number
                    }
                    const props = withDefaults(
                    defineProps<TestPropType>(),
                    {
                        name: "World",
                    }
                    );
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                <script setup lang="ts">
                    type TestPropType = {
                    name?: string
                    age?: number
                    }
                    const props = withDefaults(
                    defineProps<TestPropType>(),
                    {
                        name: "World",
                    }
                    );
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                <script setup lang="ts">
                    interface TestPropType {
                    name?
                    }
                    const props = withDefaults(
                    defineProps<TestPropType>(),
                    {
                        name: "World",
                    }
                    );
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                <script setup lang="ts">
                    interface TestPropType {
                    get name(): string
                    set name(a: string)
                    age?: number
                    }
                    const props = withDefaults(
                    defineProps<TestPropType>(),
                    {
                        'name': 'World',
                    }
                    );
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                <script setup lang="ts">
                    interface TestPropType {
                    get name(): void
                    age?: number
                    }
                    const props = withDefaults(
                    defineProps<TestPropType>(),
                    {
                        'name': 'World',
                    }
                    );
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                <script setup lang="ts">
                    const [name] = 'test'

                    interface TestPropType {
                    [name]: string
                    age?: number
                    }
                    const props = withDefaults(
                    defineProps<TestPropType>(),
                    {
                        [name]: 'World'
                    }
                    );
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            "
                <script>
                export default {
                    props: {
                    name: {
                        required: false,
                        default: 'Hello'
                    }
                    }
                }
                </script>
                ",
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "
                <script>
                export default {
                    props: ['name']
                }
                </script>
                ",
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "
                <script setup>
                    const props = defineProps({
                    name: {
                        required: false,
                        default: 'Hello'
                    }
                    })
                </script>
                ",
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            r#"
                <script setup lang="ts">
                    interface TestPropType {
                    name?: string
                    }
                    const {name="World"} = defineProps<TestPropType>();
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                <script setup lang="ts">
                    const {name="World"} = defineProps<{
                    name?: string
                    }>();
                </script>
                "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            "
                <script setup>
                    const {name='Hello'} = defineProps({
                    name: {
                        required: false
                    }
                    })
                </script>
                ",
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "   <script lang='ts'>
                export interface ComponentProps {
                    name?: string;
                }
                </script>
                <script setup lang='ts'>
                    const {name='Hello'} = defineProps<ComponentProps>()
                </script>
                ",
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
    ];

    let fail = vec![
        (
            "
                const a = defineComponent({
                    props: {
                        'name': {
                        required: true,
                        default: 'Hello'
                        }
                    }
                    })
            ",
            None,
            None,
            Some(PathBuf::from("test.ts")),
        ),
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        name: string
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          name: "World",
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        name: string | number
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          name: "World",
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        'na::me': string
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          'na::me': "World",
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      import nameType from 'name.ts';
                      interface TestPropType {
                        name: nameType
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          name: "World",
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        name
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          name: "World",
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        name
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          name: "World",
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        'na\\"me2'
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          'na\\"me2': "World",
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        foo(): void
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          foo() {console.log(123)},
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        readonly name
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          name: 'World',
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        readonly 'name'
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          'name': 'World',
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        // Unicode escape in defaults: '\u0061' matches property 'a'
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        readonly 'a'
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          '\u0061': 'World',
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        // Unicode escape in interface: '\u0061' matches 'a' in defaults
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        readonly '\u0061'
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          'a': 'World',
                        }
                      );
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            "
                    <script>
                    export default {
                      props: {
                        name: {
                          required: true,
                          default: 'Hello'
                        }
                      }
                    }
                    </script>
                  ",
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "
                    <script>
                    export default {
                      props: {
                        'name': {
                          required: true,
                          default: 'Hello'
                        }
                      }
                    }
                    </script>
                  ",
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "
                    <script>
                    import { defineComponent } from 'vue'
                    export default defineComponent({
                      props: {
                        'name': {
                          required: true,
                          default: 'Hello'
                        }
                      }
                    })
                    </script>
                  ",
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "
                    <script>
                    import { defineComponent } from 'vue'
                    export default defineComponent({
                      props: {
                        name: {
                          required: true,
                          default: 'Hello'
                        }
                      }
                    })
                    </script>
                  ",
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "
                    <script>
                    import { defineComponent } from 'vue'
                    export default defineComponent({
                      props: {
                        name: {
                          required: true,
                          default: 'Hello'
                        }
                      }
                    })
                    </script>
                  ",
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            "
                    <script setup>
                      const props = defineProps({
                        name: {
                          required: true,
                          default: 'Hello'
                        }
                      })
                    </script>
                  ",
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            r#"
                    <script setup lang="ts">
                      interface TestPropType {
                        name: string
                      }
                      const {name="World"} = defineProps<TestPropType>();
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      const {name="World"} = defineProps<{
                        name: string
                      }>();
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ), // {        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      },
        (
            r#"
                    <script setup lang="ts">
                      const {name="World"} = defineProps({
                        name: {
                          required: true,
                        }
                      });
                    </script>
                  "#,
            None, // Some(serde_json::json!([{ "autofix": true }])),
            None,
            Some(PathBuf::from("test.vue")),
        ),
        (
            r#"
                    <script setup lang="ts">
                      type TestPropType = {
                        name: string
                        age?: number
                      }
                      const props = withDefaults(
                        defineProps<TestPropType>(),
                        {
                          name: "World",
                        }
                      );
                    </script>
                  "#,
            None,
            None,
            Some(PathBuf::from("test.vue")),
        ),
    ];

    let fix = vec![
        // TypeScript interface: insert ? after key name
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                name: string
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                name?: string
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // TypeScript interface: union type
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                name: string | number
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                name?: string | number
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // TypeScript interface: imported type reference
        (
            r#"
            <script setup lang="ts">
              import nameType from 'name.ts';
              interface TestPropType {
                name: nameType
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              import nameType from 'name.ts';
              interface TestPropType {
                name?: nameType
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // TypeScript interface: bare property without type annotation
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                name
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                name?
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  name: "World",
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // TypeScript interface: string literal key
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                'na::me': string
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  'na::me': "World",
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                'na::me'?: string
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  'na::me': "World",
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // TypeScript interface: escaped quotes in property name
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                'na\"me2'
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  'na\"me2': "World",
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                'na\"me2'?
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  'na\"me2': "World",
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // TypeScript interface: method signature
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                foo(): void
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  foo() {console.log(123)},
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                foo?(): void
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  foo() {console.log(123)},
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // Unicode escape in defaults: '\u0061' matches property 'a'
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                readonly 'a'
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  '\u0061': 'World',
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                readonly 'a'?
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  '\u0061': 'World',
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // Unicode escape in interface: '\u0061' matches 'a' in defaults
        (
            r#"
            <script setup lang="ts">
              interface TestPropType {
                readonly '\u0061'
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  'a': 'World',
                }
              );
            </script>
            "#,
            r#"
            <script setup lang="ts">
              interface TestPropType {
                readonly '\u0061'?
                age?: number
              }
              const props = withDefaults(
                defineProps<TestPropType>(),
                {
                  'a': 'World',
                }
              );
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // TypeScript type literal: inline defineProps
        (
            r#"
            <script setup lang="ts">
              const {name="World"} = defineProps<{
                name: string
              }>();
            </script>
            "#,
            r#"
            <script setup lang="ts">
              const {name="World"} = defineProps<{
                name?: string
              }>();
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // Options API: change required: true to required: false
        (
            "
            <script>
            export default {
              props: {
                name: {
                  required: true,
                  default: 'Hello'
                }
              }
            }
            </script>
            ",
            "
            <script>
            export default {
              props: {
                name: {
                  required: false,
                  default: 'Hello'
                }
              }
            }
            </script>
            ",
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // Options API: quoted property key
        (
            "
            <script>
            export default {
              props: {
                'name': {
                  required: true,
                  default: 'Hello'
                }
              }
            }
            </script>
            ",
            "
            <script>
            export default {
              props: {
                'name': {
                  required: false,
                  default: 'Hello'
                }
              }
            }
            </script>
            ",
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // defineComponent: change required: true to required: false
        (
            "
            <script>
            import { defineComponent } from 'vue'
            export default defineComponent({
              props: {
                name: {
                  required: true,
                  default: 'Hello'
                }
              }
            })
            </script>
            ",
            "
            <script>
            import { defineComponent } from 'vue'
            export default defineComponent({
              props: {
                name: {
                  required: false,
                  default: 'Hello'
                }
              }
            })
            </script>
            ",
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // Script setup defineProps object: change required: true to required: false
        (
            "
            <script setup>
              const props = defineProps({
                name: {
                  required: true,
                  default: 'Hello'
                }
              })
            </script>
            ",
            "
            <script setup>
              const props = defineProps({
                name: {
                  required: false,
                  default: 'Hello'
                }
              })
            </script>
            ",
            None,
            Some(PathBuf::from("test.vue")),
        ),
        // Destructured defineProps with default value
        (
            r#"
            <script setup lang="ts">
              const {name="World"} = defineProps({
                name: {
                  required: true,
                }
              });
            </script>
            "#,
            r#"
            <script setup lang="ts">
              const {name="World"} = defineProps({
                name: {
                  required: false,
                }
              });
            </script>
            "#,
            None,
            Some(PathBuf::from("test.vue")),
        ),
    ];

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