//! Tests for the rename engine.

use super::*;

#[test]
fn capitalize_works() {
    assert_eq!(capitalize("widget"), "Widget");
    assert_eq!(capitalize(""), "");
    assert_eq!(capitalize("a"), "A");
}

#[test]
fn pluralize_regular() {
    assert_eq!(pluralize("widget"), "widgets");
    assert_eq!(pluralize("gadget"), "gadgets");
}

#[test]
fn pluralize_y_ending() {
    assert_eq!(pluralize("ability"), "abilities");
    assert_eq!(pluralize("query"), "queries");
}

#[test]
fn pluralize_s_ending() {
    assert_eq!(pluralize("class"), "classes");
}

#[test]
fn pluralize_preserves_ey_oy_ay() {
    assert_eq!(pluralize("key"), "keys");
    assert_eq!(pluralize("day"), "days");
}

#[test]
fn rename_spec_generates_variants() {
    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let from_values: Vec<&str> = spec.variants.iter().map(|v| v.from.as_str()).collect();
    assert!(from_values.contains(&"widget"));
    assert!(from_values.contains(&"Widget"));
    assert!(from_values.contains(&"WIDGET"));
    assert!(from_values.contains(&"widgets"));
    assert!(from_values.contains(&"Widgets"));
    assert!(from_values.contains(&"WIDGETS"));

    let to_values: Vec<&str> = spec.variants.iter().map(|v| v.to.as_str()).collect();
    assert!(to_values.contains(&"gadget"));
    assert!(to_values.contains(&"Gadget"));
    assert!(to_values.contains(&"GADGET"));
    assert!(to_values.contains(&"gadgets"));
    assert!(to_values.contains(&"Gadgets"));
    assert!(to_values.contains(&"GADGETS"));
}

#[test]
fn rename_spec_preserves_exact_branded_display_string() {
    let spec = RenameSpec::new(
        "Figma to WordPress",
        "Figma to WordPress Studio",
        RenameScope::All,
    );

    assert!(spec.variants.iter().any(|variant| {
        variant.label == "exact"
            && variant.from == "Figma to WordPress"
            && variant.to == "Figma to WordPress Studio"
    }));
    assert!(spec
        .variants
        .iter()
        .any(|variant| variant.from == "Figma To Word Press"));
}

#[test]
fn normal_rename_matches_branded_display_string_without_literal() {
    let dir = std::env::temp_dir().join("homeboy_refactor_branded_display_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(
        dir.join("test.md"),
        "# Figma to WordPress\n\nFigma to WordPress workshop\n",
    )
    .unwrap();

    let spec = RenameSpec::new(
        "Figma to WordPress",
        "Figma to WordPress Studio",
        RenameScope::All,
    );
    assert!(!spec.literal);

    let mut result = generate_renames(&spec, &dir);
    assert_eq!(result.edits.len(), 1);
    assert!(result.references.iter().any(|reference| {
        reference.matched == "Figma to WordPress"
            && reference.replacement == "Figma to WordPress Studio"
    }));

    apply_renames(&mut result, &dir).unwrap();
    let content = std::fs::read_to_string(dir.join("test.md")).unwrap();
    assert_eq!(
        content,
        "# Figma to WordPress Studio\n\nFigma to WordPress Studio workshop\n"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn find_references_in_temp_dir() {
    let dir = std::env::temp_dir().join("homeboy_refactor_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(
        dir.join("test.rs"),
        "pub mod widget;\nuse crate::widget::WidgetManifest;\nconst WIDGET_DIR: &str = \"widgets\";\n",
    )
    .unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let refs = find_references(&spec, &dir);

    assert!(!refs.is_empty());

    // Should find: widget (2x), Widget (1x), WIDGET (1x), widgets (1x)
    let matched: Vec<&str> = refs.iter().map(|r| r.matched.as_str()).collect();
    assert!(
        matched.contains(&"widget"),
        "Expected 'widget' in {:?}",
        matched
    );
    assert!(
        matched.contains(&"Widget"),
        "Expected 'Widget' in {:?}",
        matched
    );
    assert!(
        matched.contains(&"WIDGET"),
        "Expected 'WIDGET' in {:?}",
        matched
    );
    assert!(
        matched.contains(&"widgets"),
        "Expected 'widgets' in {:?}",
        matched
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn generate_renames_produces_edits() {
    let dir = std::env::temp_dir().join("homeboy_refactor_gen_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(dir.join("test.rs"), "pub mod widget;\n").unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let result = generate_renames(&spec, &dir);

    assert!(!result.edits.is_empty());
    assert_eq!(result.edits[0].new_content, "pub mod gadget;\n");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn generate_renames_detects_file_renames() {
    let dir = std::env::temp_dir().join("homeboy_refactor_file_rename_test");
    let sub = dir.join("widget");
    let _ = std::fs::create_dir_all(&sub);

    std::fs::write(sub.join("widget.rs"), "fn widget_init() {}\n").unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let result = generate_renames(&spec, &dir);

    assert!(!result.file_renames.is_empty());
    // Should want to rename widget/widget.rs → gadget/gadget.rs
    let rename = result
        .file_renames
        .iter()
        .find(|r| r.from.contains("widget.rs"))
        .unwrap();
    assert!(rename.to.contains("gadget.rs"));

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn path_renames_include_skipped_content_files_in_renamed_directory() {
    let dir = std::env::temp_dir().join("homeboy_refactor_skipped_path_move_test");
    let _ = std::fs::remove_dir_all(&dir);
    let old_dir = dir.join("plugins/host/studio-native/build/assets/preview");
    std::fs::create_dir_all(&old_dir).unwrap();

    std::fs::write(old_dir.join("studio-native-preview.png"), b"png bytes").unwrap();
    std::fs::write(old_dir.join("preview.js.map"), b"{\"version\":3}").unwrap();
    std::fs::write(
        dir.join("plugins/host/studio-native/.npmrc"),
        b"legacy-peer-deps=true\n",
    )
    .unwrap();
    std::fs::write(
        old_dir.join("studio-native-preview.js"),
        "studioNative();\n",
    )
    .unwrap();

    let spec = RenameSpec::literal("studio-native", "wp-build", RenameScope::All);
    let mut result = generate_renames(&spec, &dir);

    assert!(result.file_renames.iter().any(|rename| {
        rename.from == "plugins/host/studio-native/build/assets/preview/studio-native-preview.png"
            && rename.to == "plugins/host/wp-build/build/assets/preview/wp-build-preview.png"
    }));
    assert!(result.file_renames.iter().any(|rename| {
        rename.from == "plugins/host/studio-native/build/assets/preview/preview.js.map"
            && rename.to == "plugins/host/wp-build/build/assets/preview/preview.js.map"
    }));
    assert!(result.file_renames.iter().any(|rename| {
        rename.from == "plugins/host/studio-native/.npmrc"
            && rename.to == "plugins/host/wp-build/.npmrc"
    }));

    apply_renames(&mut result, &dir).unwrap();

    assert!(dir
        .join("plugins/host/wp-build/build/assets/preview/wp-build-preview.png")
        .exists());
    assert!(dir
        .join("plugins/host/wp-build/build/assets/preview/preview.js.map")
        .exists());
    assert!(dir.join("plugins/host/wp-build/.npmrc").exists());
    assert!(!dir
        .join("plugins/host/studio-native/build/assets/preview/studio-native-preview.png")
        .exists());
    assert!(!dir
        .join("plugins/host/studio-native/build/assets/preview/preview.js.map")
        .exists());
    assert!(!dir.join("plugins/host/studio-native/.npmrc").exists());

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn repo_config_dotfiles_are_content_edited() {
    let dir = std::env::temp_dir().join("homeboy_refactor_repo_config_dotfile_edit_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(
        dir.join(".gitignore"),
        "plugins/host/studio-native/build/\nplugins/host/studio-native/assets/\n",
    )
    .unwrap();

    let spec = RenameSpec::literal("studio-native", "wp-build", RenameScope::All);
    let mut result = generate_renames(&spec, &dir);

    let edit = result
        .edits
        .iter()
        .find(|edit| edit.file == ".gitignore")
        .expect(".gitignore should be content-edit eligible");
    assert!(edit.new_content.contains("plugins/host/wp-build/build/"));
    assert!(!edit.new_content.contains("studio-native"));

    apply_renames(&mut result, &dir).unwrap();

    let content = std::fs::read_to_string(dir.join(".gitignore")).unwrap();
    assert_eq!(
        content,
        "plugins/host/wp-build/build/\nplugins/host/wp-build/assets/\n"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn path_renames_prune_emptied_source_directories() {
    let dir = std::env::temp_dir().join("homeboy_refactor_prune_empty_source_dirs_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(dir.join("plugins/host/studio-native/assets")).unwrap();
    std::fs::create_dir_all(dir.join("plugins/host/studio-native/build/js")).unwrap();

    std::fs::write(
        dir.join("plugins/host/studio-native/assets/studio-native-logo.svg"),
        "<svg></svg>\n",
    )
    .unwrap();
    std::fs::write(
        dir.join("plugins/host/studio-native/build/js/app.js"),
        "console.log('ok');\n",
    )
    .unwrap();

    let spec = RenameSpec::literal("studio-native", "wp-build", RenameScope::All);
    let mut result = generate_renames(&spec, &dir);

    apply_renames(&mut result, &dir).unwrap();

    assert!(dir
        .join("plugins/host/wp-build/assets/wp-build-logo.svg")
        .exists());
    assert!(dir.join("plugins/host/wp-build/build/js/app.js").exists());
    assert!(!dir.join("plugins/host/studio-native/assets").exists());
    assert!(!dir.join("plugins/host/studio-native/build/js").exists());
    assert!(!dir.join("plugins/host/studio-native/build").exists());
    assert!(!dir.join("plugins/host/studio-native").exists());
    assert!(dir.join("plugins/host").exists());

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn path_renames_preserve_intermediate_segments_under_renamed_ancestor() {
    let dir = std::env::temp_dir().join("homeboy_refactor_nested_path_segment_test");
    let _ = std::fs::remove_dir_all(&dir);
    let dist = dir.join("plugins/host/studio-native/modules/agentic-ui-block/dist");
    std::fs::create_dir_all(&dist).unwrap();

    std::fs::write(
        dist.join("studio-native-agentic-ui-block.js"),
        "studioNativeAgenticUiBlock();\n",
    )
    .unwrap();

    let spec = RenameSpec::literal("studio-native", "wp-build", RenameScope::All);
    let mut result = generate_renames(&spec, &dir);

    let rename = result
        .file_renames
        .iter()
        .find(|rename| rename.from.ends_with("studio-native-agentic-ui-block.js"))
        .unwrap();
    assert_eq!(
        rename.from,
        "plugins/host/studio-native/modules/agentic-ui-block/dist/studio-native-agentic-ui-block.js"
    );
    assert_eq!(
        rename.to,
        "plugins/host/wp-build/modules/agentic-ui-block/dist/wp-build-agentic-ui-block.js"
    );

    apply_renames(&mut result, &dir).unwrap();

    assert!(dir
        .join("plugins/host/wp-build/modules/agentic-ui-block/dist/wp-build-agentic-ui-block.js")
        .exists());
    assert!(!dir
        .join("plugins/host/wp-build/modules/agentic-ui-block/wp-build-agentic-ui-block.js")
        .exists());
    assert!(!dir
        .join("plugins/host/studio-native/modules/agentic-ui-block/dist/studio-native-agentic-ui-block.js")
        .exists());

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn word_boundary_no_false_positives() {
    let dir = std::env::temp_dir().join("homeboy_refactor_boundary_test");
    let _ = std::fs::create_dir_all(&dir);

    // "widgets_plus" should NOT be matched as "widget" — the 's' makes it "widgets" (plural variant)
    // but "widgetry" should NOT be matched when renaming "widget"
    std::fs::write(dir.join("test.rs"), "let widgetry = true;\n").unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let refs = find_references(&spec, &dir);

    assert!(
        refs.is_empty(),
        "Should not match 'widgetry' when renaming 'widget'"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn apply_renames_writes_to_disk() {
    let dir = std::env::temp_dir().join("homeboy_refactor_apply_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(dir.join("test.rs"), "pub mod widget;\n").unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let mut result = generate_renames(&spec, &dir);

    apply_renames(&mut result, &dir).unwrap();
    assert!(result.applied);

    let content = std::fs::read_to_string(dir.join("test.rs")).unwrap();
    assert_eq!(content, "pub mod gadget;\n");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn snake_case_compounds_match() {
    // find_boundary_matches should match "widget" inside "load_widget", "is_widget_linked", etc.
    let matches = find_boundary_matches("load_widget", "widget");
    assert_eq!(matches, vec![5], "Should match 'widget' in 'load_widget'");

    let matches = find_boundary_matches("is_widget_linked", "widget");
    assert_eq!(
        matches,
        vec![3],
        "Should match 'widget' in 'is_widget_linked'"
    );

    let matches = find_boundary_matches("widget_init", "widget");
    assert_eq!(
        matches,
        vec![0],
        "Should match 'widget' at start of 'widget_init'"
    );

    let matches = find_boundary_matches("WIDGET_DIR", "WIDGET");
    assert_eq!(matches, vec![0], "Should match 'WIDGET' in 'WIDGET_DIR'");

    let matches = find_boundary_matches("THE_WIDGET_CONFIG", "WIDGET");
    assert_eq!(
        matches,
        vec![4],
        "Should match 'WIDGET' in 'THE_WIDGET_CONFIG'"
    );
}

#[test]
fn snake_case_rename_in_file() {
    let dir = std::env::temp_dir().join("homeboy_refactor_snake_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(
        dir.join("test.rs"),
        "fn load_widget() {}\nfn is_widget_linked() -> bool { true }\nconst WIDGET_DIR: &str = \"widgets\";\n",
    )
    .unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let result = generate_renames(&spec, &dir);

    assert!(!result.edits.is_empty());
    let content = &result.edits[0].new_content;
    assert!(
        content.contains("load_gadget"),
        "Expected 'load_gadget' in:\n{}",
        content
    );
    assert!(
        content.contains("is_gadget_linked"),
        "Expected 'is_gadget_linked' in:\n{}",
        content
    );
    assert!(
        content.contains("GADGET_DIR"),
        "Expected 'GADGET_DIR' in:\n{}",
        content
    );
    assert!(
        content.contains("\"gadgets\""),
        "Expected '\"gadgets\"' in:\n{}",
        content
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn node_modules_not_matched() {
    // "node_modules" should NOT have "module" matched inside it — the plural
    // variant "modules" consumes it first, but we don't want partial matches either.
    // "node_modules" as a directory name is handled by SKIP_DIRS, but in content
    // the plural variant "modules" should match (not "module" partially).
    let matches = find_boundary_matches("node_modules", "module");
    assert!(
        matches.is_empty(),
        "Should not match 'module' inside 'node_modules' — 's' follows"
    );

    // But "modules" (plural) should match
    let matches = find_boundary_matches("node_modules", "modules");
    assert_eq!(matches, vec![5], "Should match 'modules' in 'node_modules'");
}

#[test]
fn extract_field_identifier_works() {
    assert_eq!(
        extract_field_identifier("pub name: String,"),
        Some("name".to_string())
    );
    assert_eq!(
        extract_field_identifier("pub(crate) id: u32,"),
        Some("id".to_string())
    );
    assert_eq!(
        extract_field_identifier("count: usize,"),
        Some("count".to_string())
    );
    assert_eq!(
        extract_field_identifier("let value = 42;"),
        Some("value".to_string())
    );
    assert_eq!(
        extract_field_identifier("fn init("),
        Some("init".to_string())
    );
    assert_eq!(extract_field_identifier("if (enabled) {"), None);
    assert_eq!(extract_field_identifier("// a comment"), None);
    assert_eq!(extract_field_identifier("#[serde(skip)]"), None);
    assert_eq!(extract_field_identifier(""), None);
}

#[test]
fn detect_duplicate_identifiers_catches_collision() {
    let content = "struct Foo {\n    pub name: String,\n    pub name: u32,\n}\n";
    let mut warnings = Vec::new();
    detect_duplicate_identifiers("test.rs", content, &mut warnings);
    assert_eq!(warnings.len(), 1);
    assert_eq!(warnings[0].kind, "duplicate_identifier");
    assert!(warnings[0].message.contains("name"));
}

#[test]
fn detect_duplicate_identifiers_no_false_positive() {
    let content = "struct Foo {\n    pub name: String,\n    pub age: u32,\n}\n";
    let mut warnings = Vec::new();
    detect_duplicate_identifiers("test.rs", content, &mut warnings);
    assert!(warnings.is_empty());
}

#[test]
fn detect_duplicate_identifiers_ignores_separate_nested_object_literals() {
    let content = r#"function build() {
  tasks.set(smoke.id, {
id: smoke.id,
promptFile: smoke.prompt,
graderFile: smoke.check,
  });
  tasks.set(scenario.id, {
id: scenario.id,
promptFile: scenario.prompt,
graderFile: scenario.check,
  });
}
"#;
    let mut warnings = Vec::new();
    detect_duplicate_identifiers("workflow.yml", content, &mut warnings);
    assert!(warnings.is_empty());
}

#[test]
fn detect_duplicate_identifiers_scopes_sibling_object_literals() {
    let content = "const tasks = [\n    {\n        promptFile: 'a.md',\n        graderFile: 'a.test.md',\n    },\n    {\n        promptFile: 'b.md',\n        graderFile: 'b.test.md',\n    },\n];\n";
    let mut warnings = Vec::new();
    detect_duplicate_identifiers("test.js", content, &mut warnings);
    assert!(warnings.is_empty());
}

#[test]
fn detect_duplicate_identifiers_ignores_repeated_control_flow_keywords() {
    let content = r#"function run(items) {
    if (items.length === 0) {
        return;
    }
    if (items.length > 1) {
        return;
    }
}
"#;
    let mut warnings = Vec::new();
    detect_duplicate_identifiers("test.ts", content, &mut warnings);
    assert!(warnings.is_empty());
}

#[test]
fn collision_detection_file_rename_target_exists() {
    let dir = std::env::temp_dir().join("homeboy_collision_file_test");
    let _ = std::fs::create_dir_all(&dir);

    // Create both source and target files
    std::fs::write(dir.join("old.rs"), "fn old() {}\n").unwrap();
    std::fs::write(dir.join("new.rs"), "fn new() {}\n").unwrap();

    let file_renames = vec![FileRename {
        from: "old.rs".to_string(),
        to: "new.rs".to_string(),
    }];

    let warnings = detect_collisions(&[], &file_renames, &dir);
    assert_eq!(warnings.len(), 1);
    assert_eq!(warnings[0].kind, "file_collision");
    assert!(warnings[0].message.contains("new.rs"));

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn collision_detection_in_generate_renames() {
    let dir = std::env::temp_dir().join("homeboy_collision_gen_test");
    let _ = std::fs::create_dir_all(&dir);

    // This simulates the exact #284 bug: struct has both `widgets` and `gadgets` fields,
    // and renaming widget → gadget would create two `gadgets` fields.
    std::fs::write(
        dir.join("test.rs"),
        "struct Config {\n    pub widgets: Vec<String>,\n    pub gadgets: Vec<u32>,\n}\n",
    )
    .unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let result = generate_renames(&spec, &dir);

    assert!(
        !result.warnings.is_empty(),
        "Should detect duplicate 'gadgets' field"
    );
    assert!(result
        .warnings
        .iter()
        .any(|w| w.kind == "duplicate_identifier"));
    assert!(result
        .warnings
        .iter()
        .any(|w| w.message.contains("gadgets")));

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn nested_build_dir_not_skipped() {
    // scripts/build/ should be scanned (build is only skipped at root level)
    let dir = std::env::temp_dir().join("homeboy_refactor_build_dir_test");
    let sub = dir.join("scripts").join("build");
    let _ = std::fs::create_dir_all(&sub);

    std::fs::write(sub.join("setup.sh"), "WIDGET_PATH=\"$HOME\"\n").unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let refs = find_references(&spec, &dir);

    assert!(
        !refs.is_empty(),
        "Should find 'WIDGET' in scripts/build/setup.sh"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn root_build_dir_still_skipped() {
    // build/ at root should still be skipped
    let dir = std::env::temp_dir().join("homeboy_refactor_root_build_test");
    let build_dir = dir.join("build");
    let _ = std::fs::create_dir_all(&build_dir);

    std::fs::write(build_dir.join("output.rs"), "let widget = true;\n").unwrap();

    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let refs = find_references(&spec, &dir);

    assert!(
        refs.is_empty(),
        "Should NOT find refs in root-level build/ dir"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

// ====================================================================
// Literal mode tests
// ====================================================================

#[test]
fn literal_spec_has_single_variant() {
    let spec = RenameSpec::literal(
        "sampleplugin-events",
        "sample-plugin-events",
        RenameScope::All,
    );
    assert!(spec.literal);
    assert_eq!(spec.variants.len(), 1);
    assert_eq!(spec.variants[0].from, "sampleplugin-events");
    assert_eq!(spec.variants[0].to, "sample-plugin-events");
    assert_eq!(spec.variants[0].label, "literal");
}

#[test]
fn find_literal_matches_exact() {
    // Should find exact substring — no boundary detection
    let matches = find_literal_matches("sampleplugin-events is great", "sampleplugin-events");
    assert_eq!(matches, vec![0]);

    // Should match inside larger strings (no boundary filtering)
    let matches = find_literal_matches("the-sampleplugin-events-plugin", "sampleplugin-events");
    assert_eq!(matches, vec![4]);

    // Multiple occurrences
    let matches = find_literal_matches(
        "sampleplugin-events and sampleplugin-events",
        "sampleplugin-events",
    );
    assert_eq!(matches, vec![0, 24]);

    // No match
    let matches = find_literal_matches("sample-plugin-events", "sampleplugin-events");
    assert!(matches.is_empty());
}

#[test]
fn literal_mode_finds_references_in_file() {
    let dir = std::env::temp_dir().join("homeboy_refactor_literal_refs_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(
        dir.join("plugin.php"),
        "// Plugin: sampleplugin-events\ndefine('SAMPLEPLUGIN_EVENTS_VERSION', '1.0');\nfunction sampleplugin_events_init() {}\n",
    )
    .unwrap();

    // Literal mode: only exact match, no case variants
    let spec = RenameSpec::literal(
        "sampleplugin-events",
        "sample-plugin-events",
        RenameScope::All,
    );
    let refs = find_references(&spec, &dir);

    // Should find only the hyphenated form, not SAMPLEPLUGIN_EVENTS or sampleplugin_events
    assert_eq!(
        refs.len(),
        1,
        "Should find exactly 1 literal match, got: {:?}",
        refs.iter().map(|r| &r.matched).collect::<Vec<_>>()
    );
    assert_eq!(refs[0].matched, "sampleplugin-events");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn literal_mode_generates_correct_edits() {
    let dir = std::env::temp_dir().join("homeboy_refactor_literal_edit_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(
        dir.join("plugin.php"),
        "Text Domain: sampleplugin-events\nSlug: sampleplugin-events\n",
    )
    .unwrap();

    let spec = RenameSpec::literal(
        "sampleplugin-events",
        "sample-plugin-events",
        RenameScope::All,
    );
    let result = generate_renames(&spec, &dir);

    assert_eq!(result.edits.len(), 1);
    assert_eq!(result.edits[0].replacements, 2);
    assert_eq!(
        result.edits[0].new_content,
        "Text Domain: sample-plugin-events\nSlug: sample-plugin-events\n"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn literal_mode_renames_files() {
    let dir = std::env::temp_dir().join("homeboy_refactor_literal_file_rename_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(dir.join("sampleplugin-events.php"), "// main file\n").unwrap();

    let spec = RenameSpec::literal(
        "sampleplugin-events",
        "sample-plugin-events",
        RenameScope::All,
    );
    let result = generate_renames(&spec, &dir);

    assert!(
        !result.file_renames.is_empty(),
        "Should rename sampleplugin-events.php"
    );
    let rename = &result.file_renames[0];
    assert_eq!(rename.from, "sampleplugin-events.php");
    assert_eq!(rename.to, "sample-plugin-events.php");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn literal_mode_apply_writes_to_disk() {
    let dir = std::env::temp_dir().join("homeboy_refactor_literal_apply_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(dir.join("test.php"), "slug: sampleplugin-events\n").unwrap();

    let spec = RenameSpec::literal(
        "sampleplugin-events",
        "sample-plugin-events",
        RenameScope::All,
    );
    let mut result = generate_renames(&spec, &dir);

    apply_renames(&mut result, &dir).unwrap();
    assert!(result.applied);

    let content = std::fs::read_to_string(dir.join("test.php")).unwrap();
    assert_eq!(content, "slug: sample-plugin-events\n");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn literal_mode_no_boundary_filtering() {
    // Normal mode would NOT match "widget" inside "widgetry" — literal mode SHOULD
    let matches = find_literal_matches("widgetry", "widget");
    assert_eq!(
        matches,
        vec![0],
        "Literal should match 'widget' inside 'widgetry'"
    );

    // Normal mode boundary test for comparison
    let boundary_matches = find_boundary_matches("widgetry", "widget");
    assert!(
        boundary_matches.is_empty(),
        "Boundary mode should NOT match 'widget' inside 'widgetry'"
    );
}

// ====================================================================
// Word splitting tests
// ====================================================================

#[test]
fn split_words_kebab() {
    assert_eq!(split_words("wp-agent"), vec!["wp", "agent"]);
    assert_eq!(
        split_words("sample-plugin-agent"),
        vec!["sample", "plugin", "agent"]
    );
}

#[test]
fn split_words_snake() {
    assert_eq!(split_words("wp_agent"), vec!["wp", "agent"]);
    assert_eq!(
        split_words("sample_plugin_agent"),
        vec!["sample", "plugin", "agent"]
    );
}

#[test]
fn split_words_upper_snake() {
    assert_eq!(split_words("WP_AGENT"), vec!["wp", "agent"]);
    assert_eq!(
        split_words("SAMPLE_PLUGIN_AGENT"),
        vec!["sample", "plugin", "agent"]
    );
}

#[test]
fn split_words_pascal() {
    assert_eq!(split_words("WpAgent"), vec!["wp", "agent"]);
    assert_eq!(
        split_words("SamplePluginAgent"),
        vec!["sample", "plugin", "agent"]
    );
}

#[test]
fn split_words_consecutive_uppercase() {
    // WPAgent: WP is an acronym, Agent is a word
    assert_eq!(split_words("WPAgent"), vec!["wp", "agent"]);
    assert_eq!(split_words("XMLParser"), vec!["xml", "parser"]);
    assert_eq!(split_words("HTTPClient"), vec!["http", "client"]);
    // All-uppercase stays as one word (no lowercase to trigger split)
    assert_eq!(split_words("HTTP"), vec!["http"]);
}

#[test]
fn split_words_camel() {
    assert_eq!(split_words("wpAgent"), vec!["wp", "agent"]);
    assert_eq!(
        split_words("samplePluginAgent"),
        vec!["sample", "plugin", "agent"]
    );
}

#[test]
fn split_words_display() {
    assert_eq!(split_words("WP Agent"), vec!["wp", "agent"]);
    assert_eq!(
        split_words("Sample Plugin Agent"),
        vec!["sample", "plugin", "agent"]
    );
}

#[test]
fn split_words_single() {
    assert_eq!(split_words("widget"), vec!["widget"]);
    assert_eq!(split_words("Widget"), vec!["widget"]);
    assert_eq!(split_words("WIDGET"), vec!["widget"]);
}

// ====================================================================
// Cross-separator variant generation tests
// ====================================================================

#[test]
fn cross_separator_variants_from_kebab() {
    let spec = RenameSpec::new("wp-agent", "sample-plugin-agent", RenameScope::All);
    let from_values: Vec<&str> = spec.variants.iter().map(|v| v.from.as_str()).collect();
    let to_values: Vec<&str> = spec.variants.iter().map(|v| v.to.as_str()).collect();

    // Singular forms — all naming conventions
    assert!(from_values.contains(&"wp-agent"), "Missing kebab from");
    assert!(
        from_values.contains(&"Wp-Agent"),
        "Missing Title-Kebab-Case from"
    );
    assert!(from_values.contains(&"wp_agent"), "Missing snake from");
    assert!(
        from_values.contains(&"WP_AGENT"),
        "Missing UPPER_SNAKE from"
    );
    assert!(
        from_values.contains(&"Wp_Agent"),
        "Missing Title_Snake_Case from"
    );
    assert!(from_values.contains(&"WpAgent"), "Missing PascalCase from");
    assert!(from_values.contains(&"wpAgent"), "Missing camelCase from");
    assert!(from_values.contains(&"Wp Agent"), "Missing display from");

    assert!(
        to_values.contains(&"sample-plugin-agent"),
        "Missing kebab to"
    );
    assert!(
        to_values.contains(&"Sample-Plugin-Agent"),
        "Missing Title-Kebab-Case to"
    );
    assert!(
        to_values.contains(&"sample_plugin_agent"),
        "Missing snake to"
    );
    assert!(
        to_values.contains(&"SAMPLE_PLUGIN_AGENT"),
        "Missing UPPER_SNAKE to"
    );
    assert!(
        to_values.contains(&"Sample_Plugin_Agent"),
        "Missing Title_Snake_Case to"
    );
    assert!(
        to_values.contains(&"SamplePluginAgent"),
        "Missing PascalCase to"
    );
    assert!(
        to_values.contains(&"samplePluginAgent"),
        "Missing camelCase to"
    );
    assert!(
        to_values.contains(&"Sample Plugin Agent"),
        "Missing display to"
    );

    // Plural forms
    assert!(
        from_values.contains(&"wp-agents"),
        "Missing plural kebab from"
    );
    assert!(
        from_values.contains(&"Wp-Agents"),
        "Missing plural Title-Kebab-Case from"
    );
    assert!(
        from_values.contains(&"wp_agents"),
        "Missing plural snake from"
    );
    assert!(
        from_values.contains(&"WP_AGENTS"),
        "Missing plural UPPER_SNAKE from"
    );
    assert!(
        from_values.contains(&"Wp_Agents"),
        "Missing plural Title_Snake_Case from"
    );
    assert!(
        from_values.contains(&"WpAgents"),
        "Missing plural PascalCase from"
    );
}

#[test]
fn cross_separator_variants_from_pascal() {
    // Providing PascalCase input should produce the same cross-separator variants
    let spec = RenameSpec::new("WpAgent", "SamplePluginAgent", RenameScope::All);
    let from_values: Vec<&str> = spec.variants.iter().map(|v| v.from.as_str()).collect();

    assert!(from_values.contains(&"wp-agent"), "Missing kebab from");
    assert!(
        from_values.contains(&"Wp-Agent"),
        "Missing Title-Kebab-Case from"
    );
    assert!(from_values.contains(&"wp_agent"), "Missing snake from");
    assert!(
        from_values.contains(&"WP_AGENT"),
        "Missing UPPER_SNAKE from"
    );
    assert!(from_values.contains(&"WpAgent"), "Missing PascalCase from");
    assert!(from_values.contains(&"wpAgent"), "Missing camelCase from");
}

#[test]
fn cross_separator_variants_from_snake() {
    // Providing snake_case input should produce the same cross-separator variants
    let spec = RenameSpec::new("wp_agent", "sample_plugin_agent", RenameScope::All);
    let from_values: Vec<&str> = spec.variants.iter().map(|v| v.from.as_str()).collect();

    assert!(from_values.contains(&"wp-agent"), "Missing kebab from");
    assert!(
        from_values.contains(&"Wp-Agent"),
        "Missing Title-Kebab-Case from"
    );
    assert!(from_values.contains(&"wp_agent"), "Missing snake from");
    assert!(
        from_values.contains(&"WP_AGENT"),
        "Missing UPPER_SNAKE from"
    );
    assert!(from_values.contains(&"WpAgent"), "Missing PascalCase from");
}

#[test]
fn single_word_variants_dedup() {
    // For a single word, all separator joins produce the same thing
    let spec = RenameSpec::new("widget", "gadget", RenameScope::All);
    let from_values: Vec<&str> = spec.variants.iter().map(|v| v.from.as_str()).collect();

    // Should still have the core variants
    assert!(from_values.contains(&"widget"));
    assert!(from_values.contains(&"Widget"));
    assert!(from_values.contains(&"WIDGET"));
    assert!(from_values.contains(&"widgets"));
    assert!(from_values.contains(&"Widgets"));
    assert!(from_values.contains(&"WIDGETS"));

    // No duplicate entries
    let mut seen = std::collections::HashSet::new();
    for v in &spec.variants {
        assert!(seen.insert(&v.from), "Duplicate variant 'from': {}", v.from);
    }
}

// ====================================================================
// Boundary detection for consecutive-uppercase PascalCase
// ====================================================================

#[test]
fn boundary_matches_consecutive_uppercase_pascal() {
    // WPAgent → should match "Agent" at position 2
    let matches = find_boundary_matches("WPAgent", "Agent");
    assert_eq!(
        matches,
        vec![2],
        "Should match 'Agent' in 'WPAgent' at consecutive-uppercase boundary"
    );

    // WPAgent → should match "WP" at position 0
    let matches = find_boundary_matches("WPAgent", "WP");
    assert_eq!(matches, vec![0], "Should match 'WP' at start of 'WPAgent'");

    // XMLParser → should match "XML" and "Parser"
    let matches = find_boundary_matches("XMLParser", "XML");
    assert_eq!(
        matches,
        vec![0],
        "Should match 'XML' at start of 'XMLParser'"
    );

    let matches = find_boundary_matches("XMLParser", "Parser");
    assert_eq!(matches, vec![3], "Should match 'Parser' in 'XMLParser'");
}

#[test]
fn boundary_matches_wp_agent_display_name() {
    // "WP Agent" with a space — should match at word boundaries
    let matches = find_boundary_matches("Plugin: WP Agent v1", "WP Agent");
    assert_eq!(
        matches,
        vec![8],
        "Should match 'WP Agent' in display context"
    );
}

#[test]
fn cross_separator_end_to_end_rename() {
    // The real use case: rename wp-agent → sample-plugin-agent across all conventions
    let dir = std::env::temp_dir().join("homeboy_cross_sep_e2e_test");
    let _ = std::fs::create_dir_all(&dir);

    std::fs::write(
        dir.join("plugin.php"),
        concat!(
            "// Plugin: wp-agent\n",
            "namespace WpAgent;\n",
            "define('WP_AGENT_VERSION', '1.0');\n",
            "function wp_agent_init() {}\n",
            "// slug: wp-agents\n",
        ),
    )
    .unwrap();

    let spec = RenameSpec::new("wp-agent", "sample-plugin-agent", RenameScope::All);
    let result = generate_renames(&spec, &dir);

    assert!(!result.edits.is_empty(), "Should have edits");
    let content = &result.edits[0].new_content;

    assert!(
        content.contains("sample-plugin-agent"),
        "Should rename kebab: {}",
        content
    );
    assert!(
        content.contains("SamplePluginAgent"),
        "Should rename PascalCase: {}",
        content
    );
    assert!(
        content.contains("SAMPLE_PLUGIN_AGENT_VERSION"),
        "Should rename UPPER_SNAKE: {}",
        content
    );
    assert!(
        content.contains("sample_plugin_agent_init"),
        "Should rename snake_case: {}",
        content
    );
    assert!(
        content.contains("sample-plugin-agents"),
        "Should rename plural kebab: {}",
        content
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn title_snake_variant_renames_wordpress_class_names() {
    let dir = std::env::temp_dir().join("homeboy_title_snake_variant_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(
        dir.join("plugin.php"),
        "final class Studio_Native_Codebox_Client {}\n",
    )
    .unwrap();

    let spec = RenameSpec::new("studio-native", "wp-build", RenameScope::All);
    let result = generate_renames(&spec, &dir);
    assert_eq!(result.edits.len(), 1);
    let content = &result.edits[0].new_content;

    assert!(content.contains("Wp_Build_Codebox_Client"), "{}", content);
    assert!(
        !content.contains("Studio_Native_Codebox_Client"),
        "{}",
        content
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn title_kebab_variant_renames_hyphenated_doc_comments() {
    let dir = std::env::temp_dir().join("homeboy_title_kebab_variant_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(
        dir.join("plugin.php"),
        "// Studio-Native-shaped site_spec\n",
    )
    .unwrap();

    let spec = RenameSpec::new("studio-native", "wp-build", RenameScope::All);
    let result = generate_renames(&spec, &dir);
    assert_eq!(result.edits.len(), 1);
    let content = &result.edits[0].new_content;

    assert!(content.contains("Wp-Build-shaped"), "{}", content);
    assert!(!content.contains("Studio-Native-shaped"), "{}", content);

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn explicit_variant_overrides_title_kebab_auto_variant() {
    let dir = std::env::temp_dir().join("homeboy_title_kebab_explicit_variant_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(
        dir.join("plugin.php"),
        "// Studio-Native-shaped site_spec\n",
    )
    .unwrap();

    let spec =
        RenameSpec::new("studio-native", "wp-build", RenameScope::All).with_explicit_variants(
            vec![("Studio-Native".to_string(), "WordPress-Build".to_string())],
        );
    let result = generate_renames(&spec, &dir);
    assert_eq!(result.edits.len(), 1);
    let content = &result.edits[0].new_content;

    assert!(content.contains("WordPress-Build-shaped"), "{}", content);
    assert!(!content.contains("Wp-Build-shaped"), "{}", content);
    assert!(!content.contains("Studio-Native-shaped"), "{}", content);

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn explicit_variant_overrides_title_snake_auto_variant() {
    let dir = std::env::temp_dir().join("homeboy_title_snake_explicit_variant_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(
        dir.join("plugin.php"),
        "final class Studio_Native_Codebox_Client {}\n",
    )
    .unwrap();

    let spec = RenameSpec::new("studio-native", "wp-build", RenameScope::All)
        .with_explicit_variants(vec![("Studio_Native".to_string(), "WPBuild".to_string())]);
    let result = generate_renames(&spec, &dir);
    assert_eq!(result.edits.len(), 1);
    let content = &result.edits[0].new_content;

    assert!(content.contains("WPBuild_Codebox_Client"), "{}", content);
    assert!(!content.contains("Wp_Build_Codebox_Client"), "{}", content);
    assert!(
        !content.contains("Studio_Native_Codebox_Client"),
        "{}",
        content
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn explicit_variants_handle_project_specific_acronyms() {
    let dir = std::env::temp_dir().join("homeboy_explicit_variant_acronym_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(
        dir.join("plugin.php"),
        concat!(
            "// Display: Old API Client\n",
            "final class Old_API_Client {}\n"
        ),
    )
    .unwrap();

    let spec = RenameSpec::new("old-client", "new-client", RenameScope::All)
        .with_explicit_variants(vec![
            ("Old API Client".to_string(), "New API Client".to_string()),
            ("Old_API_Client".to_string(), "New_API_Client".to_string()),
        ]);

    let result = generate_renames(&spec, &dir);
    assert_eq!(result.edits.len(), 1);
    let content = &result.edits[0].new_content;

    assert!(content.contains("New API Client"), "{}", content);
    assert!(content.contains("New_API_Client"), "{}", content);
    assert!(!content.contains("Old API Client"), "{}", content);
    assert!(!content.contains("Old_API_Client"), "{}", content);

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn explicit_variants_override_auto_variants_with_same_from() {
    let dir = std::env::temp_dir().join("homeboy_explicit_variant_precedence_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    std::fs::write(dir.join("plugin.php"), "fn studio_native_gate() {}\n").unwrap();

    let spec = RenameSpec::new("studio-native", "wp-build", RenameScope::All)
        .with_explicit_variants(vec![("studio_native".to_string(), "wpbuild".to_string())]);

    let result = generate_renames(&spec, &dir);
    assert_eq!(result.edits.len(), 1);
    let content = &result.edits[0].new_content;

    assert!(content.contains("wpbuild_gate"), "{}", content);
    assert!(!content.contains("wp_build_gate"), "{}", content);
    assert!(!content.contains("studio_native_gate"), "{}", content);

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn root_artifacts_directory_is_skipped_by_default() {
    let dir = std::env::temp_dir().join("homeboy_refactor_artifacts_skip_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(dir.join("artifacts")).unwrap();
    std::fs::create_dir_all(dir.join("src")).unwrap();

    std::fs::write(dir.join("src/lib.rs"), "fn old_client() {}\n").unwrap();
    std::fs::write(dir.join("artifacts/output.rs"), "fn old_client() {}\n").unwrap();

    let spec = RenameSpec::new("old-client", "new-client", RenameScope::All);
    let result = generate_renames(&spec, &dir);

    assert_eq!(result.edits.len(), 1);
    assert_eq!(result.edits[0].file, "src/lib.rs");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn include_glob_limits_edits_to_targeted_files() {
    let dir = std::env::temp_dir().join("homeboy_refactor_target_include_test");
    let _ = std::fs::create_dir_all(dir.join("src"));
    let _ = std::fs::create_dir_all(dir.join("tests"));

    std::fs::write(dir.join("src/lib.rs"), "fn mark_item_processed() {}\n").unwrap();
    std::fs::write(
        dir.join("tests/lib_test.rs"),
        "fn test_mark_item_processed() {}\n",
    )
    .unwrap();

    let spec = RenameSpec::new(
        "mark_item_processed",
        "add_processed_item",
        RenameScope::All,
    );
    let targeting = RenameTargeting {
        include_globs: vec!["tests/**/*.rs".to_string()],
        ..RenameTargeting::default()
    };

    let result = generate_renames_with_targeting(&spec, &dir, &targeting);

    assert_eq!(result.edits.len(), 1, "Should only edit tests files");
    assert_eq!(result.edits[0].file, "tests/lib_test.rs");
    assert!(result.edits[0].new_content.contains("add_processed_item"));
    assert!(!result.edits[0].new_content.contains("mark_item_processed"));

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn exclude_glob_omits_matching_files() {
    let dir = std::env::temp_dir().join("homeboy_refactor_target_exclude_test");
    let _ = std::fs::create_dir_all(dir.join("src"));
    let _ = std::fs::create_dir_all(dir.join("tests"));

    std::fs::write(dir.join("src/lib.rs"), "fn mark_item_processed() {}\n").unwrap();
    std::fs::write(
        dir.join("tests/lib_test.rs"),
        "fn test_mark_item_processed() {}\n",
    )
    .unwrap();

    let spec = RenameSpec::new(
        "mark_item_processed",
        "add_processed_item",
        RenameScope::All,
    );
    let targeting = RenameTargeting {
        exclude_globs: vec!["src/**/*.rs".to_string()],
        ..RenameTargeting::default()
    };

    let result = generate_renames_with_targeting(&spec, &dir, &targeting);

    assert_eq!(result.edits.len(), 1, "Should skip excluded src files");
    assert_eq!(result.edits[0].file, "tests/lib_test.rs");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn no_file_renames_suppresses_path_renames() {
    let dir = std::env::temp_dir().join("homeboy_refactor_no_file_rename_test");
    let _ = std::fs::create_dir_all(&dir);
    std::fs::write(dir.join("mark_item_processed_test.rs"), "fn ok() {}\n").unwrap();

    let spec = RenameSpec::new(
        "mark_item_processed",
        "add_processed_item",
        RenameScope::All,
    );
    let targeting = RenameTargeting {
        rename_files: false,
        ..RenameTargeting::default()
    };

    let result = generate_renames_with_targeting(&spec, &dir, &targeting);
    assert!(
        result.file_renames.is_empty(),
        "File renames should be disabled when rename_files=false"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn context_key_filters_to_string_keys_only() {
    let dir = std::env::temp_dir().join("homeboy_context_key_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    let content = r#"
$handler_config = [];
$input['handler_config'] = 'value';
function foo($handler_config) {}
echo "handler_config";
$obj->handler_config = true;
"#;
    std::fs::write(dir.join("test.php"), content).unwrap();

    let mut spec = RenameSpec::literal("handler_config", "handler_configs", RenameScope::All);
    spec.rename_context = RenameContext::Key;

    let refs = find_references_with_targeting(&spec, &dir, &RenameTargeting::default());

    // Should match: 'handler_config' (string key), "handler_config" (string),
    // ->handler_config (property access)
    // Should NOT match: $handler_config (variable), $handler_config (parameter)
    let matched_lines: Vec<usize> = refs.iter().map(|r| r.line).collect();
    assert!(
        !matched_lines.contains(&2), // $handler_config = [] — variable
        "Should not match variable assignment, got matches at lines: {:?}",
        matched_lines
    );
    assert!(
        matched_lines.contains(&3), // $input['handler_config'] — string key
        "Should match string key, got matches at lines: {:?}",
        matched_lines
    );
    assert!(
        !matched_lines.contains(&4), // function foo($handler_config) — parameter
        "Should not match function parameter, got matches at lines: {:?}",
        matched_lines
    );
    assert!(
        matched_lines.contains(&5), // "handler_config" — string
        "Should match string literal, got matches at lines: {:?}",
        matched_lines
    );
    assert!(
        matched_lines.contains(&6), // ->handler_config — property access
        "Should match property access, got matches at lines: {:?}",
        matched_lines
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn context_variable_filters_to_variables_only() {
    let dir = std::env::temp_dir().join("homeboy_context_var_test");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    let content = r#"
$handler_config = [];
$input['handler_config'] = 'value';
$obj->handler_config = true;
"#;
    std::fs::write(dir.join("test.php"), content).unwrap();

    let mut spec = RenameSpec::literal("handler_config", "handler_configs", RenameScope::All);
    spec.rename_context = RenameContext::Variable;

    let refs = find_references_with_targeting(&spec, &dir, &RenameTargeting::default());

    let matched_lines: Vec<usize> = refs.iter().map(|r| r.line).collect();
    assert!(
        matched_lines.contains(&2), // $handler_config — variable
        "Should match PHP variable, got matches at lines: {:?}",
        matched_lines
    );
    assert!(
        !matched_lines.contains(&3), // 'handler_config' — string key, not variable
        "Should not match string key, got matches at lines: {:?}",
        matched_lines
    );
    assert!(
        !matched_lines.contains(&4), // ->handler_config — property access, not variable
        "Should not match property access, got matches at lines: {:?}",
        matched_lines
    );

    let _ = std::fs::remove_dir_all(&dir);
}
