//! Duplication detection — find identical and near-identical functions across
//! source files, and duplicated code blocks within a single method.
//!
//! Uses method body hashes from fingerprinting to detect exact duplicates,
//! and structural hashes (identifiers/literals normalized to positional tokens)
//! to detect near-duplicates — functions with identical control flow that differ
//! only in variable names, constant references, or string values.
//!
//! Four outputs:
//! - `detect_duplicates()` → flat `Vec<Finding>` for exact duplicates
//! - `detect_duplicate_groups()` → structured `Vec<DuplicateGroup>` for the fixer
//! - `detect_near_duplicates()` → flat `Vec<Finding>` for structural near-duplicates
//! - `detect_intra_method_duplicates()` → duplicated blocks within a single method

use std::collections::{HashMap, HashSet};

use super::conventions::AuditFinding;
use super::findings::{Finding, Severity};
use super::fingerprint::FileFingerprint;
use super::idiomatic::is_trivial_method;
use super::walker::is_test_path;
use crate::core::component::DuplicationDetectorConfig;

/// Minimum number of locations for a function to count as duplicated.
const MIN_DUPLICATE_LOCATIONS: usize = 2;

/// A group of files containing an identical function.
///
/// The fixer uses this to keep the canonical copy and remove the rest.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DuplicateGroup {
    /// The duplicated function name.
    pub function_name: String,
    /// File chosen to keep the function (canonical location).
    pub canonical_file: String,
    /// Files where the duplicate should be removed and replaced with an import.
    pub remove_from: Vec<String>,
}

/// Build grouped duplication data from fingerprints.
///
/// For each group of identical functions, picks a canonical file (shortest
/// path, then alphabetical) and lists the rest as removal targets.
fn build_groups(fingerprints: &[&FileFingerprint]) -> HashMap<(String, String), Vec<String>> {
    let mut hash_groups: HashMap<(String, String), Vec<String>> = HashMap::new();

    for fp in fingerprints {
        for (method_name, body_hash) in &fp.method_hashes {
            hash_groups
                .entry((method_name.clone(), body_hash.clone()))
                .or_default()
                .push(fp.relative_path.clone());
        }
    }

    hash_groups
}

/// Pick the canonical file from a list of locations.
///
/// Heuristics (in order):
/// 1. Files in a `utils/` directory are preferred (already shared)
/// 2. Shortest path (most general module)
/// 3. Alphabetical (deterministic tiebreaker)
fn pick_canonical(locations: &[String]) -> String {
    let mut sorted = locations.to_vec();
    sorted.sort_by(|a, b| {
        let a_utils = a.contains("/utils/") || a.contains("/utils.");
        let b_utils = b.contains("/utils/") || b.contains("/utils.");
        // utils files first
        b_utils
            .cmp(&a_utils)
            // then shortest path
            .then_with(|| a.len().cmp(&b.len()))
            // then alphabetical
            .then_with(|| a.cmp(b))
    });
    sorted[0].clone()
}

/// Detect duplicate groups with canonical file selection.
///
/// Returns structured data the fixer uses to remove duplicates.
pub(crate) fn detect_duplicate_groups(fingerprints: &[&FileFingerprint]) -> Vec<DuplicateGroup> {
    let hash_groups = build_groups(fingerprints);
    let mut groups = Vec::new();

    for ((method_name, _hash), locations) in &hash_groups {
        if locations.len() < MIN_DUPLICATE_LOCATIONS {
            continue;
        }

        let canonical = pick_canonical(locations);
        let mut remove_from: Vec<String> = locations
            .iter()
            .filter(|f| **f != canonical)
            .cloned()
            .collect();
        remove_from.sort();

        groups.push(DuplicateGroup {
            function_name: method_name.clone(),
            canonical_file: canonical,
            remove_from,
        });
    }

    groups.sort_by(|a, b| a.function_name.cmp(&b.function_name));
    groups
}

/// Detect duplicated functions across all fingerprinted files.
///
/// Groups functions by their body hash. When two or more files contain a
/// function with the same name and the same normalized body hash, a finding
/// is emitted for each location.
/// Detect exact function body duplicates across files.
///
/// `convention_methods` are excluded — identical implementations across convention-
/// following files are expected behavior (e.g. `__construct`, `checkPermission`,
/// interface methods with identical bodies).
pub(crate) fn detect_duplicates(
    fingerprints: &[&FileFingerprint],
    convention_methods: &std::collections::HashSet<String>,
) -> Vec<Finding> {
    let hash_groups = build_groups(fingerprints);
    let mut findings = Vec::new();

    for ((method_name, _hash), locations) in &hash_groups {
        if locations.len() < MIN_DUPLICATE_LOCATIONS {
            continue;
        }

        // Skip convention-expected methods — identical implementations are by design.
        if convention_methods.contains(method_name) {
            continue;
        }

        let test_only_duplicate = locations.iter().all(|file| is_test_path(file));
        let severity = if test_only_duplicate {
            Severity::Info
        } else {
            Severity::Warning
        };
        let suggestion = if test_only_duplicate {
            format!(
                "Function `{}` has identical body in {} test files. Consider a shared test helper if the duplication grows or starts obscuring test intent.",
                method_name,
                locations.len()
            )
        } else {
            format!(
                "Function `{}` has identical body in {} files. \
             Extract to a shared module and import it.",
                method_name,
                locations.len()
            )
        };

        // Emit one finding per file that has the duplicate
        for file in locations {
            let mut also_in_vec: Vec<_> =
                locations.iter().filter(|f| *f != file).cloned().collect();
            also_in_vec.sort();
            let also_in = also_in_vec.join(", ");

            findings.push(Finding {
                convention: "duplication".to_string(),
                severity: severity.clone(),
                file: file.clone(),
                description: format!("Duplicate function `{}` — also in {}", method_name, also_in),
                suggestion: suggestion.clone(),
                kind: AuditFinding::DuplicateFunction,
            });
        }
    }

    // Sort by file path then description for deterministic output
    findings.sort_by(|a, b| {
        a.file
            .cmp(&b.file)
            .then_with(|| a.description.cmp(&b.description))
    });
    findings
}

// ============================================================================
// Near-Duplicate Detection (structural similarity)
// ============================================================================

/// Names that are too generic to flag as near-duplicates.
/// These appear in many files with completely unrelated implementations.
const GENERIC_NAMES: &[&str] = &[
    "run", "new", "default", "build", "list", "show", "set", "get", "delete", "remove", "clear",
    "create", "update", "status", "search", "find", "read", "write", "rename", "init", "test",
    "fmt", "from", "into", "clone", "drop", "display", "parse", "validate", "execute", "handle",
    "process", "merge", "resolve", "pin", "plan",
];

/// Minimum body line count — skip trivial functions (1-2 line bodies).
/// Functions like `fn default_true() -> bool { true }` are too small
/// to meaningfully refactor into shared code with a parameter.
///
/// Counted against `count_body_lines`, which returns the count of lines
/// strictly between the opening and closing braces (so a single-line body
/// is 0 and the standard three-line shape is 1).
const MIN_BODY_LINES: usize = 3;

/// Build structural hash groups from fingerprints.
///
/// Groups functions by (name, structural_hash), returning only groups
/// where the exact body hashes differ (otherwise they'd already be caught
/// by the exact-duplicate detector).
fn build_structural_groups(
    fingerprints: &[&FileFingerprint],
) -> HashMap<(String, String), Vec<(String, String)>> {
    // Collect: (fn_name, structural_hash) → [(file, body_hash), ...]
    let mut groups: HashMap<(String, String), Vec<(String, String)>> = HashMap::new();

    for fp in fingerprints {
        for (method_name, struct_hash) in &fp.structural_hashes {
            groups
                .entry((method_name.clone(), struct_hash.clone()))
                .or_default()
                .push((
                    fp.relative_path.clone(),
                    fp.method_hashes
                        .get(method_name)
                        .cloned()
                        .unwrap_or_default(),
                ));
        }
    }

    groups
}

/// Check if a file path looks like a CLI command module.
///
/// Command modules (`src/commands/*.rs`) are expected to have identically-
/// named functions (`run`, `list`, etc.) with completely different bodies.
fn is_command_file(path: &str) -> bool {
    path.contains("/commands/") || path.starts_with("commands/")
}

/// Count the body lines of a function in a file's structural hash data.
///
/// Returns the count of lines **strictly between** the line containing the
/// opening `{` and the line containing the matching `}` — the actual body,
/// not the wrapping span. So:
///
/// - `fn x() -> u32 { 0 }` (single-line body, both braces on the same line)
///   returns **0** — there are no lines strictly between the braces.
/// - The standard three-line shape
///   ```text
///   fn x() -> u32 {
///       0
///   }
///   ```
///   returns **1** — exactly the one body line.
/// - A genuine N-statement body returns ~N.
///
/// Returns 0 if the function is not found or its content is empty. The
/// previous implementation returned the **inclusive line span** from `fn`
/// to the closing brace, which off-by-twoed three-line delegation methods
/// like `pub fn len(&self) -> usize { self.inner.len() }` to a count of 3
/// and slipped them past the `< MIN_BODY_LINES` filter (#1517).
fn count_body_lines(fp: &FileFingerprint, method_name: &str) -> usize {
    let pattern = format!("fn {}", method_name);
    let lines: Vec<&str> = fp.content.lines().collect();
    let mut start = None;

    for (i, line) in lines.iter().enumerate() {
        if line.contains(&pattern) {
            start = Some(i);
            break;
        }
    }

    let Some(start_idx) = start else { return 0 };

    let mut brace_depth = 0i32;
    let mut open_line: Option<usize> = None;
    for (offset, line) in lines[start_idx..].iter().enumerate() {
        let line_idx = start_idx + offset;
        for ch in line.chars() {
            if ch == '{' {
                if open_line.is_none() {
                    open_line = Some(line_idx);
                }
                brace_depth += 1;
            } else if ch == '}' {
                brace_depth -= 1;
                if open_line.is_some() && brace_depth == 0 {
                    let open = open_line.unwrap();
                    return line_idx.saturating_sub(open).saturating_sub(1);
                }
            }
        }
    }

    0
}

/// Detect structural near-duplicates across all fingerprinted files.
///
/// Groups functions by (name, structural_hash). When two or more files
/// contain a function with the same name and the same structural hash
/// but *different* exact body hashes, it means the functions have
/// identical control flow but differ in identifiers/constants.
///
/// Filters out:
/// - Functions already caught by exact-duplicate detection
/// - Generic names (`run`, `list`, `show`, etc.)
/// - Universally-idiomatic method names (`len`, `is_empty`, `iter`, `new`,
///   `default`, `from`, `into`, `clone`, `fmt`, etc. — see
///   `super::idiomatic::is_trivial_method`)
/// - Command/core delegation pairs (command module ↔ core module)
/// - Trivial functions (fewer than `MIN_BODY_LINES` body lines, where the
///   body line count is *strictly between the braces* — so a single-line
///   body is 0 and the standard three-line shape is 1)
pub(crate) fn detect_near_duplicates(fingerprints: &[&FileFingerprint]) -> Vec<Finding> {
    let structural_groups = build_structural_groups(fingerprints);
    let exact_groups = build_groups(fingerprints);

    // Collect exact-duplicate (name, hash) pairs for exclusion
    let exact_duplicate_names: std::collections::HashSet<String> = exact_groups
        .iter()
        .filter(|(_, locs)| locs.len() >= MIN_DUPLICATE_LOCATIONS)
        .map(|((name, _), _)| name.clone())
        .collect();

    let mut findings = Vec::new();

    for ((method_name, _struct_hash), file_hashes) in &structural_groups {
        // Need at least 2 locations
        if file_hashes.len() < MIN_DUPLICATE_LOCATIONS {
            continue;
        }

        // Skip if already an exact duplicate
        if exact_duplicate_names.contains(method_name) {
            continue;
        }

        // Skip generic names
        if GENERIC_NAMES.contains(&method_name.as_str()) {
            continue;
        }

        // Skip universally-idiomatic method names. `len`, `is_empty`, `iter`,
        // `new`, `default`, `from`, `into`, `clone`, `fmt`, `as_str`,
        // `to_string`, etc. are *expected* to have boilerplate-shaped bodies
        // across unrelated types — every collection wrapper looks the same,
        // and Clippy's `len_without_is_empty` lint actually *requires* you to
        // pair `len` with `is_empty`. Flagging these as duplication is a
        // false positive (#1517). Predicate is shared with `test_coverage`
        // via `super::idiomatic::is_trivial_method`.
        if is_trivial_method(method_name) {
            continue;
        }

        // Check that exact hashes actually differ (otherwise exact detection covers it)
        let unique_body_hashes: std::collections::HashSet<&str> =
            file_hashes.iter().map(|(_, h)| h.as_str()).collect();
        if unique_body_hashes.len() < 2 {
            continue;
        }

        let files: Vec<&str> = file_hashes.iter().map(|(f, _)| f.as_str()).collect();

        // Filter: skip if all files are command modules (delegation pattern)
        if files.iter().all(|f| is_command_file(f)) {
            continue;
        }

        // Filter: skip command↔core pairs where one is in commands/ and another in core/
        // These are the delegation pattern — the command calls the core function.
        let has_command = files.iter().any(|f| is_command_file(f));
        let has_non_command = files.iter().any(|f| !is_command_file(f));
        if has_command && has_non_command && files.len() == 2 {
            continue;
        }

        // Filter: skip trivial functions (< MIN_BODY_LINES)
        let body_lines: Vec<usize> = files
            .iter()
            .filter_map(|file_path| {
                fingerprints
                    .iter()
                    .find(|fp| fp.relative_path == *file_path)
                    .map(|fp| count_body_lines(fp, method_name))
            })
            .collect();
        if body_lines.iter().all(|&l| l < MIN_BODY_LINES) {
            continue;
        }

        let suggestion = format!(
            "Function `{}` has identical structure in {} files but different \
             identifiers/constants. Consider extracting shared logic into a \
             parameterized function.",
            method_name,
            files.len()
        );

        for (file, _body_hash) in file_hashes {
            let mut also_in_vec: Vec<&str> = file_hashes
                .iter()
                .filter(|(f, _)| f != file)
                .map(|(f, _)| f.as_str())
                .collect();
            also_in_vec.sort();
            let also_in = also_in_vec.join(", ");

            findings.push(Finding {
                convention: "near-duplication".to_string(),
                severity: Severity::Info,
                file: file.clone(),
                description: format!(
                    "Near-duplicate `{}` — structurally identical to {}",
                    method_name, also_in
                ),
                suggestion: suggestion.clone(),
                kind: AuditFinding::NearDuplicate,
            });
        }
    }

    findings.sort_by(|a, b| {
        a.file
            .cmp(&b.file)
            .then_with(|| a.description.cmp(&b.description))
    });
    findings
}

// ============================================================================
// Intra-Method Duplication Detection
// ============================================================================

/// Minimum number of non-blank, non-comment lines for a block to be
/// considered meaningful. Blocks shorter than this are too trivial to flag.
const MIN_INTRA_BLOCK_LINES: usize = 5;

/// Detect duplicated code blocks within the same method/function.
///
/// For each method in each file, extracts the method body from the file
/// content and uses a sliding window of `MIN_INTRA_BLOCK_LINES` normalized
/// lines. When the same window hash appears at two non-overlapping positions
/// within one method, it means a block of code was copy-pasted (merge
/// artifacts, copy-paste errors, etc.).
pub(crate) fn detect_intra_method_duplicates(fingerprints: &[&FileFingerprint]) -> Vec<Finding> {
    let mut findings = Vec::new();

    for fp in fingerprints {
        if fp.content.is_empty() {
            continue;
        }

        let file_lines: Vec<&str> = fp.content.lines().collect();

        for method_name in &fp.methods {
            let Some((body_start, body_end)) = find_method_body(&file_lines, method_name) else {
                continue;
            };

            // Extract body lines (excluding the opening/closing brace lines)
            if body_start + 1 >= body_end {
                continue;
            }
            let body_lines: Vec<&str> = file_lines[body_start + 1..body_end].to_vec();

            if body_lines.len() < MIN_INTRA_BLOCK_LINES * 2 {
                // Body too short to contain two meaningful duplicate blocks
                continue;
            }

            // Build list of (original_body_index, normalized_text) for non-blank
            // non-comment lines
            let normalized: Vec<(usize, String)> = body_lines
                .iter()
                .enumerate()
                .filter_map(|(i, line)| {
                    let trimmed = line.trim();
                    if trimmed.is_empty() || is_comment_only(trimmed) {
                        None
                    } else {
                        Some((i, normalize_line(trimmed)))
                    }
                })
                .collect();

            if normalized.len() < MIN_INTRA_BLOCK_LINES * 2 {
                continue;
            }

            // Hash each sliding window of MIN_INTRA_BLOCK_LINES consecutive
            // normalized lines. Store (hash, start_body_idx, end_body_idx).
            let mut window_hashes: Vec<(u64, usize, usize)> = Vec::new();

            for win_start in 0..=normalized.len() - MIN_INTRA_BLOCK_LINES {
                let win_end = win_start + MIN_INTRA_BLOCK_LINES;
                let mut hasher = std::hash::DefaultHasher::new();
                for (_, norm_line) in &normalized[win_start..win_end] {
                    std::hash::Hash::hash(norm_line, &mut hasher);
                }
                let hash = std::hash::Hasher::finish(&hasher);

                let orig_start = normalized[win_start].0;
                let orig_end = normalized[win_end - 1].0;

                window_hashes.push((hash, orig_start, orig_end));
            }

            // Group by hash, look for non-overlapping pairs
            let mut hash_positions: HashMap<u64, Vec<(usize, usize)>> = HashMap::new();
            for (hash, start, end) in &window_hashes {
                hash_positions
                    .entry(*hash)
                    .or_default()
                    .push((*start, *end));
            }

            let mut reported = false;
            let mut suppressed_ranges: Vec<(usize, usize)> = Vec::new();

            let mut duplicate_windows: Vec<&Vec<(usize, usize)>> =
                hash_positions.values().collect();
            duplicate_windows.sort_by_key(|positions| positions.first().copied());

            for positions in duplicate_windows {
                if reported || positions.len() < 2 {
                    continue;
                }

                let first = positions[0];
                for other in &positions[1..] {
                    // Non-overlapping: second block starts after first block ends
                    if other.0 <= first.1 {
                        continue;
                    }

                    if is_inside_suppressed_range(first, &suppressed_ranges)
                        || is_inside_suppressed_range(*other, &suppressed_ranges)
                    {
                        continue;
                    }

                    // Extend the match: keep sliding forward while lines match
                    let first_norm_idx = normalized
                        .iter()
                        .position(|(i, _)| *i == first.0)
                        .unwrap_or(0);
                    let other_norm_idx = normalized
                        .iter()
                        .position(|(i, _)| *i == other.0)
                        .unwrap_or(0);

                    let mut match_len = MIN_INTRA_BLOCK_LINES;
                    while first_norm_idx + match_len < normalized.len()
                        && other_norm_idx + match_len < normalized.len()
                        && first_norm_idx + match_len < other_norm_idx
                    {
                        if normalized[first_norm_idx + match_len].1
                            == normalized[other_norm_idx + match_len].1
                        {
                            match_len += 1;
                        } else {
                            break;
                        }
                    }

                    // Suppress structural-syntax-only windows. Match-arm tails
                    // (`},`, `)?;`, `Ok((...))`, closing brace, bare-identifier
                    // struct-literal fields) repeat naturally across sibling
                    // dispatch branches in `run_*` functions — they're not
                    // merge artifacts or copy-paste, they're Rust syntax.
                    // A block is worth flagging only if it contains at least
                    // one logic-bearing line.
                    if is_structural_syntax_only(&normalized, first_norm_idx, match_len) {
                        continue;
                    }

                    if is_branch_shape_repetition(&body_lines, first, *other, match_len)
                        || is_low_information_literal_or_error_block(
                            &normalized,
                            first_norm_idx,
                            match_len,
                        )
                    {
                        suppressed_ranges
                            .push((first.0, normalized[first_norm_idx + match_len - 1].0));
                        suppressed_ranges
                            .push((other.0, normalized[other_norm_idx + match_len - 1].0));
                        continue;
                    }

                    // Convert body-relative line numbers to 1-indexed file lines
                    let first_file_line = body_start + 1 + first.0 + 1;
                    let other_file_line = body_start + 1 + other.0 + 1;

                    findings.push(Finding {
                        convention: "intra-method-duplication".to_string(),
                        severity: Severity::Warning,
                        file: fp.relative_path.clone(),
                        description: format!(
                            "Duplicated block in `{}` — {} identical lines at line {} and line {}",
                            method_name, match_len, first_file_line, other_file_line
                        ),
                        suggestion: format!(
                            "Function `{}` contains a duplicated code block ({} lines). \
                             This is often a merge artifact or copy-paste error. \
                             Remove the duplicate or extract shared logic.",
                            method_name, match_len
                        ),
                        kind: AuditFinding::IntraMethodDuplicate,
                    });
                    reported = true;
                    break;
                }

                if reported {
                    break;
                }
            }
        }
    }

    findings.sort_by(|a, b| {
        a.file
            .cmp(&b.file)
            .then_with(|| a.description.cmp(&b.description))
    });
    findings
}

/// Find the body of a method/function in the file lines.
///
/// Returns `(open_brace_line, close_brace_line)` — the line indices of the
/// opening and closing braces. Searches for `function <name>` or `fn <name>`.
fn find_method_body(lines: &[&str], method_name: &str) -> Option<(usize, usize)> {
    let fn_pattern_php = format!("function {}", method_name);
    let fn_pattern_rust = format!("fn {}", method_name);

    let mut start_line = None;
    for (i, line) in lines.iter().enumerate() {
        if line.contains(&fn_pattern_php) || line.contains(&fn_pattern_rust) {
            start_line = Some(i);
            break;
        }
    }

    let start = start_line?;

    // Find opening brace from the function declaration line
    let mut brace_line = None;
    for (offset, line) in lines[start..].iter().enumerate() {
        if line.contains('{') {
            brace_line = Some(start + offset);
            break;
        }
    }

    let open_line = brace_line?;

    // Track brace depth to find closing brace
    let mut depth = 0i32;
    let mut found_open = false;
    for (i, line) in lines[open_line..].iter().enumerate() {
        for ch in line.chars() {
            if ch == '{' {
                depth += 1;
                found_open = true;
            } else if ch == '}' {
                depth -= 1;
            }
        }
        if found_open && depth == 0 {
            return Some((open_line, open_line + i));
        }
    }

    None
}

/// Check if a line is comment-only (PHP, Rust, or shell style).
fn is_comment_only(trimmed: &str) -> bool {
    trimmed.starts_with("//")
        || trimmed.starts_with("/*")
        || trimmed.starts_with('*')
        || trimmed.starts_with('#')
}

/// Normalize a line for hashing: collapse whitespace, lowercase.
fn normalize_line(line: &str) -> String {
    line.split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
        .to_lowercase()
}

/// Return true when the window `normalized[start..start+len]` is pure
/// syntactic scaffolding with no logic-bearing content.
///
/// A window is scaffolding when **every** line is one of:
/// - pure punctuation closers (`}`, `},`, `)?;`, etc.)
/// - a single identifier, optionally trailed by `,` (struct-literal or
///   destructuring fields)
/// - common match-arm glue (`=> {`, `} => {`)
///
/// **and** none of the lines contain logic signals (`=`, `let `, `if `,
/// `for `, `while `, `match `, `return`, or a function-call shape
/// `foo(` / `foo::bar(`). If a single line in the window carries any
/// logic signal, the window is not scaffolding and gets flagged normally.
///
/// Match-arm tails (`)?;`, `Ok((x, 0))`, struct-literal closers) repeated
/// across sibling arms of a dispatch `match` are structural noise, not
/// duplication — this filter stops them from tripping the detector.
fn is_structural_syntax_only(normalized: &[(usize, String)], start: usize, len: usize) -> bool {
    let end = (start + len).min(normalized.len());
    if start >= end {
        return false;
    }
    let window = &normalized[start..end];

    // If any line in the window looks logical, window is not scaffolding.
    if window.iter().any(|(_, line)| has_logic_signal(line)) {
        return false;
    }

    // Every line must match a known scaffolding shape.
    window.iter().all(|(_, line)| is_scaffolding_line(line))
}

/// Lines that look like they do real work: assignment, control flow, or
/// a user function call that isn't a dispatch-return wrapper.
fn has_logic_signal(normalized: &str) -> bool {
    let t = normalized.trim();

    // Assignment or `let` binding.
    if t.contains(" = ") || t.starts_with("let ") {
        return true;
    }

    // Control flow keywords (normalized to lowercase by the caller).
    for kw in ["if ", "for ", "while ", "match ", "return ", "loop ", "?;"] {
        if t.contains(kw) && !matches!(t, ")?;" | "})?;") {
            return true;
        }
    }

    // Function / method calls that aren't bare dispatch-return wrappers.
    // `ok(...)`, `err(...)`, `some(...)`, `none` by themselves are scaffolding
    // (return-tail on a match arm); anything else with parens is real work.
    if t.contains('(') {
        let before_paren = t.split('(').next().unwrap_or("");
        let head = before_paren.trim_end_matches(':').trim_end_matches(':');
        let head = head.trim();
        let is_return_wrapper = matches!(head, "ok" | "err" | "some")
            || head.ends_with(" ok")
            || head.ends_with(" err")
            || head.ends_with(" some");
        if !is_return_wrapper {
            return true;
        }
    }

    false
}

/// Does this normalized line match a known scaffolding shape?
fn is_scaffolding_line(normalized: &str) -> bool {
    let t = normalized.trim();
    if t.is_empty() {
        return true;
    }

    // Pure-punctuation closers: `}`, `},`, `)?;`, `))`, etc.
    if t.chars()
        .all(|c| matches!(c, '}' | ')' | '?' | ';' | ',' | '('))
    {
        return true;
    }

    // Bare identifier (optionally trailing comma) — struct-literal field or
    // destructure.
    let core = t.trim_end_matches(',');
    if !core.is_empty() && core.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
        return true;
    }

    // Dispatch-return tails: `ok(...)`, `err(...)`, `some(...)`, `none`
    // (optionally with trailing `?`, `;`, `,`).
    let core = t.trim_end_matches([',', ';', '?']);
    if core == "none"
        || core.starts_with("ok(")
        || core.starts_with("err(")
        || core.starts_with("some(")
    {
        return true;
    }

    // Match-arm glue.
    if t.ends_with("=> {") || t == "} => {" || t == "_ => {" {
        return true;
    }

    false
}

/// Repeated blocks in sibling `if` / `else if` / `else` arms are usually local
/// branch shape, not high-confidence copy/paste. Keep this deliberately narrow:
/// long blocks can still indicate real duplication, and adjacent repeated logic
/// outside branch arms is still reported.
fn is_branch_shape_repetition(
    body_lines: &[&str],
    first: (usize, usize),
    other: (usize, usize),
    match_len: usize,
) -> bool {
    if match_len > 12 || first.1 >= other.0 || other.0 > body_lines.len() {
        return false;
    }

    body_lines[first.1 + 1..other.0]
        .iter()
        .any(|line| is_branch_separator(line.trim()))
}

fn is_inside_suppressed_range(candidate: (usize, usize), ranges: &[(usize, usize)]) -> bool {
    ranges
        .iter()
        .any(|(start, end)| candidate.0 >= *start && candidate.1 <= *end)
}

fn is_branch_separator(trimmed: &str) -> bool {
    trimmed.starts_with("} else")
        || trimmed.starts_with("else ")
        || trimmed.starts_with("elseif ")
        || trimmed.starts_with("} elseif")
        || trimmed.ends_with("=> {")
        || trimmed.starts_with("} => {")
}

/// Suppress low-information literal/envelope repeats: DTO tails full of
/// `None`/`Default::default()` and repeated error constructors. These are common
/// review-noise patterns where extraction usually hides branch intent.
fn is_low_information_literal_or_error_block(
    normalized: &[(usize, String)],
    start: usize,
    len: usize,
) -> bool {
    let end = (start + len).min(normalized.len());
    if start >= end {
        return false;
    }

    let window = &normalized[start..end];
    let low_info_lines = window
        .iter()
        .filter(|(_, line)| is_low_information_literal_or_error_line(line))
        .count();

    low_info_lines >= MIN_INTRA_BLOCK_LINES && low_info_lines * 100 / window.len() >= 80
}

fn is_low_information_literal_or_error_line(normalized: &str) -> bool {
    let t = normalized.trim().trim_end_matches(',');

    if t.is_empty() || is_scaffolding_line(t) {
        return true;
    }

    if t == "0" || t == "..default::default()" {
        return true;
    }

    if is_neutral_struct_field(t) {
        return true;
    }

    if is_error_envelope_line(t) {
        return true;
    }

    if is_simple_argument_line(t) {
        return true;
    }

    false
}

fn is_simple_argument_line(line: &str) -> bool {
    let mut value = line.trim();
    value = value.strip_prefix("&mut ").unwrap_or(value);
    value = value.strip_prefix('&').unwrap_or(value).trim();

    if is_simple_identifier_path(value) {
        return true;
    }

    if value.ends_with(".clone()") || value.ends_with(".to_string()") {
        return true;
    }

    if let Some((left, right)) = value.split_once(" + ") {
        return is_simple_identifier_path(left.trim()) && right.trim().parse::<u64>().is_ok();
    }

    false
}

fn is_simple_identifier_path(value: &str) -> bool {
    !value.is_empty()
        && value
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.'))
        && value.chars().any(|c| c.is_ascii_alphabetic() || c == '_')
}

fn is_neutral_struct_field(line: &str) -> bool {
    let Some((_field, value)) = line.split_once(':') else {
        return false;
    };
    let value = value.trim();

    value == "none"
        || value == "default::default()"
        || value == "false"
        || value == "0"
        || value.ends_with(".clone()")
        || value.ends_with(".to_string()")
        || value.starts_with("some(")
        || is_simple_argument_line(value)
}

fn is_error_envelope_line(line: &str) -> bool {
    line.contains("error::")
        || line.contains("::error")
        || line.contains("internal_io(")
        || line.starts_with("format!(")
        || line.starts_with("some(")
}

// ============================================================================
// Parallel Implementation Detection (call-sequence similarity)
// ============================================================================

/// Minimum number of function calls in a method body to consider it for
/// parallel implementation detection. Trivial methods (< 4 calls) are
/// too simple to meaningfully abstract.
const MIN_CALL_COUNT: usize = 4;

/// Minimum Jaccard similarity (|intersection| / |union|) between two
/// call sets to flag as a parallel implementation.
const MIN_JACCARD_SIMILARITY: f64 = 0.5;

/// Minimum longest-common-subsequence ratio to flag as parallel.
/// This captures sequential ordering — two methods that call helpers
/// in the same order score higher than ones that share calls but in
/// a different order.
const MIN_LCS_RATIO: f64 = 0.5;

/// Minimum number of shared (intersecting) calls between two methods
/// to flag as a parallel implementation. This prevents false positives
/// from methods that share only 1-2 trivial calls like `to_string`.
const MIN_SHARED_CALLS: usize = 3;

/// Minimum number of methods a call name must appear in before it can be
/// treated as corpus-common scaffolding for parallel-implementation scoring.
const MIN_COMMON_CALL_METHODS: usize = 8;

/// Minimum share of methods a call name must appear in before it can be
/// treated as corpus-common scaffolding for parallel-implementation scoring.
const COMMON_CALL_METHOD_RATIO: f64 = 0.10;

/// Raised Jaccard floor for two `StraightLine` bodies that share calls.
///
/// Without a loop or recursion two functions that overlap on stdlib
/// helpers (e.g. `fs::copy`, `create_dir_all`) carry weak workflow
/// signal — they are usually small focused helpers that happen to share
/// one stdlib pair. Force them to clear a much higher bar before flagging.
/// Loop/recursion pairs keep the standard `MIN_JACCARD_SIMILARITY`.
const STRAIGHT_LINE_JACCARD_FLOOR: f64 = 0.7;

/// Common plumbing calls that are useful in a method body but too generic to
/// carry signal for workflow-level similarity. Keep these out of the scoring
/// pass so filesystem scans, command wrappers, and terminal renderers do not
/// look like extractable parallel implementations.
const PLUMBING_CALLS: &[&str] = &[
    "args",
    "current_dir",
    "execute",
    "failure",
    "fix_deployed_permissions",
    "from_utf8_lossy",
    "is_dir",
    "is_terminal",
    "max",
    "output",
    "path",
    "quote_path",
    "read_dir",
    "read_to_string",
    "render_map",
    "run_git",
    "stderr",
    "success",
    "to_str",
];

/// Ubiquitous stdlib/trait method calls that appear in almost every function
/// and carry no signal for parallel implementation detection. Two functions
/// both calling `.to_string()` does not mean they implement the same workflow.
const TRIVIAL_CALLS: &[&str] = &[
    "to_string",
    "to_owned",
    "to_lowercase",
    "to_uppercase",
    "clone",
    "default",
    "new",
    "len",
    "is_empty",
    "is_some",
    "is_none",
    "is_ok",
    "is_err",
    "unwrap",
    "unwrap_or",
    "unwrap_or_default",
    "unwrap_or_else",
    "expect",
    "lines",
    "next",
    "ok_or_else",
    "as_str",
    "as_ref",
    "as_deref",
    "into",
    "from",
    "iter",
    "into_iter",
    "collect",
    "map",
    "filter",
    "any",
    "all",
    "find",
    "contains",
    "push",
    "pop",
    "insert",
    "remove",
    "extend",
    "join",
    "split",
    "split_whitespace",
    "trim",
    "starts_with",
    "ends_with",
    "strip_prefix",
    "strip_suffix",
    "replace",
    "display",
    "write",
    "read",
    "flush",
    "ok",
    "err",
    "map_err",
    "and_then",
    "or_else",
    "flatten",
    "take",
    "skip",
    "chain",
    "zip",
    "enumerate",
    "cloned",
    "copied",
    "rev",
    "sort",
    "sort_by",
    "dedup",
    "retain",
    "get",
    "set",
    "entry",
    "or_insert",
    "or_insert_with",
    "keys",
    "values",
    "exists",
    "parent",
    "file_name",
    "extension",
    "with_extension",
];

/// Generic, language-agnostic structural shape of a function body.
///
/// Used as a gate before flagging two methods as parallel implementations:
/// a 22-line straight-line copy helper and a recursive directory walk can
/// share the same call set (`copy`, `create_dir_all`, …) yet have nothing
/// in common at the workflow level. Requiring shape compatibility kills
/// that false positive without leaning on language-specific identifiers.
///
/// Detection is purely lexical so it works for Rust, Python, JS, PHP, Go,
/// etc. — see [`detect_body_shape`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BodyShape {
    /// Body contains at least one loop construct or iterator-pipeline call.
    Looping,
    /// Body calls its own function name (direct recursion).
    Recursive,
    /// Body contains neither a loop nor a self-call.
    StraightLine,
}

impl BodyShape {
    /// True when two shapes can plausibly implement the same workflow.
    ///
    /// Looping and Recursive are both "iterates over something" and freely
    /// match each other — a recursive directory walk and a `for` loop over
    /// `read_dir` are genuinely interchangeable. StraightLine only matches
    /// itself; pairing a straight-line helper with a loop is the FP shape.
    fn compatible_with(self, other: BodyShape) -> bool {
        use BodyShape::*;
        matches!(
            (self, other),
            (Looping, Looping)
                | (Looping, Recursive)
                | (Recursive, Looping)
                | (Recursive, Recursive)
                | (StraightLine, StraightLine)
        )
    }
}

/// Per-method call sequence extracted from file content.
#[derive(Debug)]
struct MethodCallSequence {
    file: String,
    method: String,
    /// Ordered list of function/method calls made in the body.
    calls: Vec<String>,
    /// Generic structural shape of the body — used as a gate before flagging
    /// two methods as parallel implementations.
    shape: BodyShape,
}

/// Generic looping markers — substrings that indicate the body iterates over
/// something. Covers control-flow keywords (`for`, `while`, `loop`,
/// `foreach`) shared by most languages and common iterator-pipeline calls
/// from Rust, JS, Python, PHP, and Go. Match is whitespace/`(`-bounded so
/// substrings like `format!` (containing `for`) do not register.
const LOOPING_MARKERS: &[&str] = &[
    "for ",
    "for(",
    "while ",
    "while(",
    "loop {",
    "loop{",
    "foreach ",
    "foreach(",
    ".iter()",
    ".into_iter()",
    ".iter_mut()",
    ".for_each(",
    ".map(",
    ".filter(",
    ".fold(",
    ".flat_map(",
    ".reduce(",
    ".for_each (",
    "forEach(",
    "range(",
];

/// Detect the body shape of a function body purely from text.
///
/// Generic by construction — uses substrings (`for`, `while`, `loop`,
/// `.map(`, `.filter(`, `forEach(`, `range(`, …) that exist in every
/// mainstream language, plus a self-call probe (`<method_name>(`) for
/// recursion. No AST, no language-specific identifiers.
fn detect_body_shape(body: &str, method_name: &str) -> BodyShape {
    let has_loop = LOOPING_MARKERS.iter().any(|marker| body.contains(marker));
    let has_self_call = contains_self_call(body, method_name);

    match (has_loop, has_self_call) {
        // Recursive wins over Looping for reporting purposes only when there
        // is no loop; if both are present we still want to flag it as Looping
        // (loops are the dominant signal). For the compatibility gate the
        // distinction does not matter — Looping and Recursive are mutually
        // compatible.
        (true, _) => BodyShape::Looping,
        (false, true) => BodyShape::Recursive,
        (false, false) => BodyShape::StraightLine,
    }
}

/// Return true if `body` contains a call to `method_name` (i.e. direct
/// recursion). The check is the function name followed by `(`, with the
/// preceding character either absent or a non-identifier byte so that
/// `do_thing` does not match `redo_thing`.
fn contains_self_call(body: &str, method_name: &str) -> bool {
    if method_name.is_empty() {
        return false;
    }
    let bytes = body.as_bytes();
    let needle = method_name.as_bytes();
    if needle.len() >= bytes.len() {
        return false;
    }

    let mut i = 0;
    while i + needle.len() < bytes.len() {
        if &bytes[i..i + needle.len()] == needle {
            let after = bytes[i + needle.len()];
            let before_ok = i == 0 || {
                let b = bytes[i - 1];
                !(b.is_ascii_alphanumeric() || b == b'_')
            };
            if after == b'(' && before_ok {
                return true;
            }
        }
        i += 1;
    }
    false
}

/// Extract function call names from a code block.
///
/// Matches patterns like `function_name(`, `self.method(`, `Type::method(`.
/// Returns the called name (without receiver/namespace prefix).
///
/// `extra_trivial` is an extension-supplied set of additional trivial call
/// names that augment the built-in `TRIVIAL_CALLS` floor. Core never inspects
/// these strings — they are merged with the generic floor and used opaquely.
fn extract_calls_from_body(body: &str, extra_trivial: &HashSet<&str>) -> Vec<String> {
    let mut calls = Vec::new();

    for line in body.lines() {
        let trimmed = line.trim();
        // Skip comments
        if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
            continue;
        }

        // Find all `identifier(` patterns
        let chars: Vec<char> = trimmed.chars().collect();
        let mut i = 0;
        while i < chars.len() {
            // Look for `(`
            if chars[i] == '(' && i > 0 {
                // Walk backwards to find the identifier
                let end = i;
                let mut start = i;
                while start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') {
                    start -= 1;
                }
                if start < end {
                    let name: String = chars[start..end].iter().collect();
                    // Skip language keywords, control flow, and trivial stdlib calls
                    if !is_keyword(&name)
                        && !name.is_empty()
                        && !TRIVIAL_CALLS.contains(&name.as_str())
                        && !extra_trivial.contains(name.as_str())
                    {
                        calls.push(name);
                    }
                }
            }
            i += 1;
        }
    }

    calls
}

fn corpus_common_calls(sequences: &[MethodCallSequence]) -> HashSet<String> {
    if sequences.len() < MIN_COMMON_CALL_METHODS {
        return HashSet::new();
    }

    let mut method_counts: HashMap<&str, usize> = HashMap::new();
    for sequence in sequences {
        let unique_calls: HashSet<&str> = sequence.calls.iter().map(|call| call.as_str()).collect();
        for call in unique_calls {
            *method_counts.entry(call).or_insert(0) += 1;
        }
    }

    let ratio_floor = (sequences.len() as f64 * COMMON_CALL_METHOD_RATIO).ceil() as usize;
    let count_floor = MIN_COMMON_CALL_METHODS.max(ratio_floor);

    method_counts
        .into_iter()
        .filter(|&(_call, count)| count >= count_floor)
        .map(|(call, _count)| call.to_string())
        .collect()
}

fn signal_calls(
    calls: &[String],
    extra_plumbing: &HashSet<&str>,
    common_calls: &HashSet<String>,
) -> Vec<String> {
    calls
        .iter()
        .filter(|call| {
            !PLUMBING_CALLS.contains(&call.as_str())
                && !extra_plumbing.contains(call.as_str())
                && !common_calls.contains(call.as_str())
        })
        .cloned()
        .collect()
}

/// Check if a name is a language keyword (not a function call).
fn is_keyword(name: &str) -> bool {
    matches!(
        name,
        "if" | "else"
            | "for"
            | "while"
            | "loop"
            | "match"
            | "return"
            | "let"
            | "mut"
            | "const"
            | "fn"
            | "pub"
            | "use"
            | "mod"
            | "struct"
            | "enum"
            | "impl"
            | "trait"
            | "type"
            | "where"
            | "self"
            | "Self"
            | "super"
            | "crate"
            | "as"
            | "in"
            | "ref"
            | "Some"
            | "None"
            | "Ok"
            | "Err"
            | "true"
            | "false"
            | "assert"
            | "assert_eq"
            | "assert_ne"
            | "println"
            | "eprintln"
            | "format"
            | "vec"
            | "todo"
            | "unimplemented"
            | "unreachable"
            | "panic"
            | "dbg"
    )
}

/// Extract per-method call sequences from all fingerprints.
///
/// `extra_trivial` is an extension-supplied set of additional call names to
/// treat as trivial during call-sequence extraction. It is merged with the
/// built-in `TRIVIAL_CALLS` floor — this function never interprets the
/// strings, it only filters them out of the recorded sequence.
fn extract_call_sequences(
    fingerprints: &[&FileFingerprint],
    extra_trivial: &HashSet<&str>,
) -> Vec<MethodCallSequence> {
    let mut sequences = Vec::new();

    for fp in fingerprints {
        if fp.content.is_empty() {
            continue;
        }

        // Skip test files entirely — test code is expected to mirror production
        // call patterns and flagging it as "parallel implementation" is noise.
        if super::walker::is_test_path(&fp.relative_path) {
            continue;
        }

        let lines: Vec<&str> = fp.content.lines().collect();

        for method_name in &fp.methods {
            // Skip generic names — they're expected to have similar call patterns
            if GENERIC_NAMES.contains(&method_name.as_str()) {
                continue;
            }

            // Skip test methods (inline #[cfg(test)] modules)
            if method_name.starts_with("test_") {
                continue;
            }

            let Some((body_start, body_end)) = find_method_body(&lines, method_name) else {
                continue;
            };

            if body_start + 1 >= body_end {
                continue;
            }

            let body: String = lines[body_start + 1..body_end].join("\n");
            let calls = extract_calls_from_body(&body, extra_trivial);
            let shape = detect_body_shape(&body, method_name);

            if calls.len() >= MIN_CALL_COUNT {
                sequences.push(MethodCallSequence {
                    file: fp.relative_path.clone(),
                    method: method_name.clone(),
                    calls,
                    shape,
                });
            }
        }
    }

    sequences
}

/// Compute Jaccard similarity between two sets.
fn jaccard_similarity(a: &[String], b: &[String]) -> f64 {
    let set_a: std::collections::HashSet<&str> = a.iter().map(|s| s.as_str()).collect();
    let set_b: std::collections::HashSet<&str> = b.iter().map(|s| s.as_str()).collect();

    let intersection = set_a.intersection(&set_b).count();
    let union = set_a.union(&set_b).count();

    if union == 0 {
        0.0
    } else {
        intersection as f64 / union as f64
    }
}

/// Compute longest common subsequence length between two sequences.
fn lcs_length(a: &[String], b: &[String]) -> usize {
    let m = a.len();
    let n = b.len();
    let mut dp = vec![vec![0usize; n + 1]; m + 1];

    for i in 1..=m {
        for j in 1..=n {
            if a[i - 1] == b[j - 1] {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
            }
        }
    }

    dp[m][n]
}

/// Compute LCS ratio: 2 * LCS / (len(a) + len(b)).
fn lcs_ratio(a: &[String], b: &[String]) -> f64 {
    let total = a.len() + b.len();
    if total == 0 {
        return 0.0;
    }
    2.0 * lcs_length(a, b) as f64 / total as f64
}

/// Detect parallel implementations across files.
///
/// Compares all method pairs (in different files) by their call sequences.
/// When two methods make a similar set of calls in a similar order — but
/// have different names and different exact implementations — they're
/// likely parallel implementations of the same workflow that should be
/// abstracted into a shared parameterized function.
///
/// Filters out:
/// - Methods in the same file
/// - Generic names (run, new, build, etc.)
/// - Methods with fewer than MIN_CALL_COUNT calls
/// - Pairs already caught by exact or near-duplicate detection
/// - Pairs below both similarity thresholds
/// Detect parallel implementations — methods with similar call patterns across files.
///
/// `convention_methods` contains method names that are expected by discovered conventions.
/// When both methods in a pair are convention-expected, the pair is skipped — similar call
/// patterns are the expected behavior for convention-following code, not a finding.
///
/// `detector_config` carries extension-supplied trivial/plumbing call name lists
/// that augment the built-in generic floors. Core never interprets these strings;
/// they are merged into the existing filters.
pub(crate) fn detect_parallel_implementations(
    fingerprints: &[&FileFingerprint],
    convention_methods: &std::collections::HashSet<String>,
    detector_config: &DuplicationDetectorConfig,
) -> Vec<Finding> {
    let extra_trivial: HashSet<&str> = detector_config
        .trivial_calls
        .iter()
        .map(|s| s.as_str())
        .collect();
    let extra_plumbing: HashSet<&str> = detector_config
        .plumbing_calls
        .iter()
        .map(|s| s.as_str())
        .collect();

    let sequences = extract_call_sequences(fingerprints, &extra_trivial);
    let common_calls = corpus_common_calls(&sequences);

    // Build sets of already-flagged pairs (exact + near duplicates) to avoid double-flagging
    let exact_groups = build_groups(fingerprints);
    let exact_dup_fns: std::collections::HashSet<String> = exact_groups
        .iter()
        .filter(|(_, locs)| locs.len() >= MIN_DUPLICATE_LOCATIONS)
        .map(|((name, _), _)| name.clone())
        .collect();

    let mut findings = Vec::new();
    let mut reported_pairs: std::collections::HashSet<(String, String)> =
        std::collections::HashSet::new();

    for i in 0..sequences.len() {
        for j in (i + 1)..sequences.len() {
            let a = &sequences[i];
            let b = &sequences[j];

            // Skip same file
            if a.file == b.file {
                continue;
            }

            // Skip if same function name (already caught by other detectors)
            if a.method == b.method {
                continue;
            }

            // Skip if either function is an exact duplicate
            if exact_dup_fns.contains(&a.method) || exact_dup_fns.contains(&b.method) {
                continue;
            }

            // Skip if either method is convention-expected — its call pattern is shaped
            // by the convention, so similar patterns with other methods are expected.
            if convention_methods.contains(&a.method) || convention_methods.contains(&b.method) {
                continue;
            }

            // Skip already-reported pairs (both directions)
            let pair_key = if a.file < b.file || (a.file == b.file && a.method < b.method) {
                (
                    format!("{}::{}", a.file, a.method),
                    format!("{}::{}", b.file, b.method),
                )
            } else {
                (
                    format!("{}::{}", b.file, b.method),
                    format!("{}::{}", a.file, a.method),
                )
            };
            if reported_pairs.contains(&pair_key) {
                continue;
            }

            let a_signal = signal_calls(&a.calls, &extra_plumbing, &common_calls);
            let b_signal = signal_calls(&b.calls, &extra_plumbing, &common_calls);

            if a_signal.len() < MIN_CALL_COUNT || b_signal.len() < MIN_CALL_COUNT {
                continue;
            }

            // Body-shape gate (issue #2334): a parallel-implementation finding
            // must reflect a shared workflow, not just a shared call set. Two
            // bodies with incompatible shapes (e.g. a single-file copy helper
            // vs a recursive directory walk) are not the same workflow even
            // when they share `fs::copy` and `create_dir_all`.
            if !a.shape.compatible_with(b.shape) {
                continue;
            }

            // For two StraightLine bodies the shared call set is the only
            // signal we have, so raise the Jaccard floor — a small focused
            // helper that overlaps with another small helper on a couple of
            // stdlib calls is too weak to flag.
            let jaccard_floor = if matches!(
                (a.shape, b.shape),
                (BodyShape::StraightLine, BodyShape::StraightLine)
            ) {
                STRAIGHT_LINE_JACCARD_FLOOR
            } else {
                MIN_JACCARD_SIMILARITY
            };

            let jaccard = jaccard_similarity(&a_signal, &b_signal);
            let lcs = lcs_ratio(&a_signal, &b_signal);

            if jaccard >= jaccard_floor && lcs >= MIN_LCS_RATIO {
                // Find the shared calls for the description
                let set_a: std::collections::HashSet<&str> =
                    a_signal.iter().map(|s| s.as_str()).collect();
                let set_b: std::collections::HashSet<&str> =
                    b_signal.iter().map(|s| s.as_str()).collect();
                let mut shared: Vec<&&str> = set_a.intersection(&set_b).collect();

                // Require a minimum absolute number of shared calls.
                // Jaccard/LCS alone can trigger on tiny overlaps (2 shared out of 4 total).
                if shared.len() < MIN_SHARED_CALLS {
                    continue;
                }

                reported_pairs.insert(pair_key);
                shared.sort();
                let shared_preview: String = shared
                    .iter()
                    .take(5)
                    .map(|s| format!("`{}`", s))
                    .collect::<Vec<_>>()
                    .join(", ");
                let extra = if shared.len() > 5 {
                    format!(" (+{} more)", shared.len() - 5)
                } else {
                    String::new()
                };

                let suggestion = format!(
                    "`{}` and `{}` follow the same call pattern (Jaccard: {:.0}%, sequence: {:.0}%). \
                     Consider extracting the shared workflow into a parameterized function.",
                    a.method,
                    b.method,
                    jaccard * 100.0,
                    lcs * 100.0
                );

                // Emit finding for file A
                findings.push(Finding {
                    convention: "parallel-implementation".to_string(),
                    severity: Severity::Info,
                    file: a.file.clone(),
                    description: format!(
                        "Parallel implementation: `{}` has similar call pattern to `{}` in {} — shared calls: {}{}",
                        a.method, b.method, b.file, shared_preview, extra
                    ),
                    suggestion: suggestion.clone(),
                    kind: AuditFinding::ParallelImplementation,
                });

                // Emit finding for file B
                findings.push(Finding {
                    convention: "parallel-implementation".to_string(),
                    severity: Severity::Info,
                    file: b.file.clone(),
                    description: format!(
                        "Parallel implementation: `{}` has similar call pattern to `{}` in {} — shared calls: {}{}",
                        b.method, a.method, a.file, shared_preview, extra
                    ),
                    suggestion,
                    kind: AuditFinding::ParallelImplementation,
                });
            }
        }
    }

    findings.sort_by(|a, b| {
        a.file
            .cmp(&b.file)
            .then_with(|| a.description.cmp(&b.description))
    });
    findings
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::code_audit::conventions::Language;

    fn make_fingerprint(path: &str, methods: &[&str], hashes: &[(&str, &str)]) -> FileFingerprint {
        make_fingerprint_with_structural(path, methods, hashes, &[])
    }

    fn make_fingerprint_with_structural(
        path: &str,
        methods: &[&str],
        hashes: &[(&str, &str)],
        structural: &[(&str, &str)],
    ) -> FileFingerprint {
        FileFingerprint {
            relative_path: path.to_string(),
            language: Language::Rust,
            methods: methods.iter().map(|s| s.to_string()).collect(),
            method_hashes: hashes
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
            structural_hashes: structural
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
            ..Default::default()
        }
    }

    #[test]
    fn detects_exact_duplicate() {
        let fp1 = make_fingerprint("src/utils/io.rs", &["is_zero"], &[("is_zero", "abc123")]);
        let fp2 = make_fingerprint(
            "src/utils/validation.rs",
            &["is_zero"],
            &[("is_zero", "abc123")],
        );

        let findings = detect_duplicates(&[&fp1, &fp2], &std::collections::HashSet::new());

        assert_eq!(findings.len(), 2, "Should emit one finding per location");
        assert!(findings
            .iter()
            .all(|f| f.kind == AuditFinding::DuplicateFunction));
        assert!(findings.iter().any(|f| f.file == "src/utils/io.rs"));
        assert!(findings.iter().any(|f| f.file == "src/utils/validation.rs"));
        assert!(findings[0].description.contains("is_zero"));
    }

    #[test]
    fn duplicate_functions_under_tests_are_info_findings() {
        let fp1 = make_fingerprint(
            "tests/import/ability-smoke.php",
            &["imp_assert"],
            &[("imp_assert", "abc123")],
        );
        let fp2 = make_fingerprint(
            "tests/import/adapter-smoke.php",
            &["imp_assert"],
            &[("imp_assert", "abc123")],
        );

        let findings = detect_duplicates(&[&fp1, &fp2], &std::collections::HashSet::new());

        assert_eq!(findings.len(), 2);
        assert!(findings
            .iter()
            .all(|finding| finding.severity == Severity::Info));
        assert!(findings
            .iter()
            .all(|finding| finding.suggestion.contains("shared test helper")));
    }

    #[test]
    fn no_duplicates_different_hashes() {
        let fp1 = make_fingerprint("src/a.rs", &["process"], &[("process", "hash_a")]);
        let fp2 = make_fingerprint("src/b.rs", &["process"], &[("process", "hash_b")]);

        let findings = detect_duplicates(&[&fp1, &fp2], &std::collections::HashSet::new());
        assert!(
            findings.is_empty(),
            "Different hashes should not flag duplicates"
        );
    }

    #[test]
    fn no_duplicates_single_location() {
        let fp = make_fingerprint("src/only.rs", &["unique_fn"], &[("unique_fn", "abc123")]);

        let findings = detect_duplicates(&[&fp], &std::collections::HashSet::new());
        assert!(findings.is_empty(), "Single location is not a duplicate");
    }

    #[test]
    fn three_way_duplicate() {
        let fp1 = make_fingerprint("src/a.rs", &["helper"], &[("helper", "same_hash")]);
        let fp2 = make_fingerprint("src/b.rs", &["helper"], &[("helper", "same_hash")]);
        let fp3 = make_fingerprint("src/c.rs", &["helper"], &[("helper", "same_hash")]);

        let findings = detect_duplicates(&[&fp1, &fp2, &fp3], &std::collections::HashSet::new());

        assert_eq!(findings.len(), 3, "Should flag all 3 locations");
        assert!(findings[0].suggestion.contains("3 files"));
    }

    #[test]
    fn empty_method_hashes_no_findings() {
        let fp1 = make_fingerprint("src/a.rs", &["foo", "bar"], &[]);
        let fp2 = make_fingerprint("src/b.rs", &["foo", "bar"], &[]);

        let findings = detect_duplicates(&[&fp1, &fp2], &std::collections::HashSet::new());
        assert!(
            findings.is_empty(),
            "No hashes means no duplication findings"
        );
    }

    #[test]
    fn mixed_duplicates_and_unique() {
        let fp1 = make_fingerprint(
            "src/a.rs",
            &["shared", "unique_a"],
            &[("shared", "same"), ("unique_a", "hash_a")],
        );
        let fp2 = make_fingerprint(
            "src/b.rs",
            &["shared", "unique_b"],
            &[("shared", "same"), ("unique_b", "hash_b")],
        );

        let findings = detect_duplicates(&[&fp1, &fp2], &std::collections::HashSet::new());

        assert_eq!(findings.len(), 2, "Only 'shared' should be flagged");
        assert!(findings.iter().all(|f| f.description.contains("shared")));
    }

    // ========================================================================
    // DuplicateGroup / canonical selection tests
    // ========================================================================

    #[test]
    fn group_picks_canonical_by_shortest_path() {
        let fp1 = make_fingerprint("src/core/deep/nested/helper.rs", &["foo"], &[("foo", "h1")]);
        let fp2 = make_fingerprint("src/utils.rs", &["foo"], &[("foo", "h1")]);

        let groups = detect_duplicate_groups(&[&fp1, &fp2]);

        assert_eq!(groups.len(), 1);
        assert_eq!(groups[0].canonical_file, "src/utils.rs");
        assert_eq!(
            groups[0].remove_from,
            vec!["src/core/deep/nested/helper.rs"]
        );
    }

    #[test]
    fn group_prefers_utils_directory() {
        let fp1 = make_fingerprint("src/core/a.rs", &["shared"], &[("shared", "h1")]);
        let fp2 = make_fingerprint("src/utils/helpers.rs", &["shared"], &[("shared", "h1")]);

        let groups = detect_duplicate_groups(&[&fp1, &fp2]);

        assert_eq!(groups.len(), 1);
        assert_eq!(groups[0].canonical_file, "src/utils/helpers.rs");
        assert_eq!(groups[0].remove_from, vec!["src/core/a.rs"]);
    }

    #[test]
    fn group_alphabetical_tiebreaker() {
        let fp1 = make_fingerprint("src/b.rs", &["dup"], &[("dup", "h1")]);
        let fp2 = make_fingerprint("src/a.rs", &["dup"], &[("dup", "h1")]);

        let groups = detect_duplicate_groups(&[&fp1, &fp2]);

        assert_eq!(groups.len(), 1);
        assert_eq!(groups[0].canonical_file, "src/a.rs");
    }

    #[test]
    fn group_three_way_has_two_removals() {
        let fp1 = make_fingerprint("src/a.rs", &["f"], &[("f", "h")]);
        let fp2 = make_fingerprint("src/b.rs", &["f"], &[("f", "h")]);
        let fp3 = make_fingerprint("src/c.rs", &["f"], &[("f", "h")]);

        let groups = detect_duplicate_groups(&[&fp1, &fp2, &fp3]);

        assert_eq!(groups.len(), 1);
        assert_eq!(groups[0].remove_from.len(), 2);
        assert!(!groups[0].remove_from.contains(&groups[0].canonical_file));
    }

    // ========================================================================
    // Near-duplicate detection tests
    // ========================================================================

    /// Helper to build a fingerprint with content for body-line counting.
    fn make_fp_with_content(
        path: &str,
        content: &str,
        hashes: &[(&str, &str)],
        structural: &[(&str, &str)],
    ) -> FileFingerprint {
        let mut fp = make_fingerprint_with_structural(path, &[], hashes, structural);
        fp.content = content.to_string();
        fp
    }

    #[test]
    fn near_duplicate_detected_when_structural_match_but_exact_differs() {
        // cache_path in two files: same structure, different constants.
        // Use a 3-body-line shape so the function clears MIN_BODY_LINES
        // (the trivial-body filter); the structural twins differ only by
        // the constant referenced.
        let content_a = "fn cache_path() -> Option<PathBuf> {\n    let base = paths::homeboy().ok()?;\n    let file = base.join(CACHE_A);\n    Some(file)\n}\n";
        let content_b = "fn cache_path() -> Option<PathBuf> {\n    let base = paths::homeboy().ok()?;\n    let file = base.join(CACHE_B);\n    Some(file)\n}\n";

        let fp1 = make_fp_with_content(
            "src/core/update_check.rs",
            content_a,
            &[("cache_path", "hash_a")],
            &[("cache_path", "SAME_STRUCT")],
        );
        let fp2 = make_fp_with_content(
            "src/core/ext_update_check.rs",
            content_b,
            &[("cache_path", "hash_b")],
            &[("cache_path", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);

        assert_eq!(findings.len(), 2, "Should flag both locations");
        assert!(findings
            .iter()
            .all(|f| f.kind == AuditFinding::NearDuplicate));
        assert!(findings[0].description.contains("cache_path"));
        assert_eq!(findings[0].severity, Severity::Info);
    }

    #[test]
    fn near_duplicate_skips_exact_duplicates() {
        // If exact hashes match, exact-duplicate detector already handles it
        let fp1 = make_fingerprint_with_structural(
            "src/a.rs",
            &["helper"],
            &[("helper", "SAME")],
            &[("helper", "SAME_STRUCT")],
        );
        let fp2 = make_fingerprint_with_structural(
            "src/b.rs",
            &["helper"],
            &[("helper", "SAME")],
            &[("helper", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert!(findings.is_empty(), "Exact duplicates should be excluded");
    }

    #[test]
    fn near_duplicate_skips_generic_names() {
        let content = "fn run() {\n    do_something();\n    do_more();\n}\n";
        let fp1 = make_fp_with_content(
            "src/core/a.rs",
            content,
            &[("run", "hash_a")],
            &[("run", "SAME_STRUCT")],
        );
        let fp2 = make_fp_with_content(
            "src/core/b.rs",
            content,
            &[("run", "hash_b")],
            &[("run", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert!(
            findings.is_empty(),
            "'run' is a generic name — should be skipped"
        );
    }

    #[test]
    fn near_duplicate_skips_command_core_pairs() {
        let content = "fn deploy_site() {\n    connect();\n    upload();\n    verify();\n}\n";
        let fp1 = make_fp_with_content(
            "src/commands/deploy.rs",
            content,
            &[("deploy_site", "hash_a")],
            &[("deploy_site", "SAME_STRUCT")],
        );
        let fp2 = make_fp_with_content(
            "src/core/deploy.rs",
            content,
            &[("deploy_site", "hash_b")],
            &[("deploy_site", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert!(findings.is_empty(), "Command↔core pair should be skipped");
    }

    #[test]
    fn near_duplicate_skips_trivial_functions() {
        // default_true is only 1 line — too trivial to refactor
        let content = "fn default_true() -> bool { true }\n";
        let fp1 = make_fp_with_content(
            "src/core/defaults.rs",
            content,
            &[("default_true", "hash_a")],
            &[("default_true", "SAME_STRUCT")],
        );
        let fp2 = make_fp_with_content(
            "src/core/project.rs",
            content,
            &[("default_true", "hash_b")],
            &[("default_true", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert!(findings.is_empty(), "Trivial functions should be skipped");
    }

    #[test]
    fn near_duplicate_not_skipped_for_multi_line_core_functions() {
        // Non-trivial functions in core/ (not commands/) SHOULD be flagged
        let content = "fn cache_path() -> Option<PathBuf> {\n    let base = paths::homeboy()?;\n    let file = base.join(FILENAME);\n    Some(file)\n}\n";
        let fp1 = make_fp_with_content(
            "src/core/update.rs",
            content,
            &[("cache_path", "hash_a")],
            &[("cache_path", "SAME_STRUCT")],
        );
        let fp2 = make_fp_with_content(
            "src/core/ext_update.rs",
            content,
            &[("cache_path", "hash_b")],
            &[("cache_path", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert_eq!(
            findings.len(),
            2,
            "Non-trivial core↔core near-duplicates should be flagged"
        );
    }

    #[test]
    fn near_duplicate_skips_all_command_files() {
        // Multiple command files with same structural hash — normal pattern
        let content = "fn components() {\n    let list = config::list();\n    for item in list {\n        output::print(item);\n    }\n}\n";
        let fp1 = make_fp_with_content(
            "src/commands/fleet.rs",
            content,
            &[("components", "hash_a")],
            &[("components", "SAME_STRUCT")],
        );
        let fp2 = make_fp_with_content(
            "src/commands/project.rs",
            content,
            &[("components", "hash_b")],
            &[("components", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert!(findings.is_empty(), "All-commands group should be skipped");
    }

    // ========================================================================
    // count_body_lines — measures body lines strictly between braces (#1517)
    // ========================================================================

    #[test]
    fn count_body_lines_zero_for_single_line_body() {
        // `fn x() -> u32 { 0 }` — opening and closing brace on the same line.
        // Zero lines strictly between them, so zero body lines.
        let content = "fn x() -> u32 { 0 }\n";
        let mut fp = make_fingerprint("src/x.rs", &["x"], &[]);
        fp.content = content.to_string();

        assert_eq!(
            count_body_lines(&fp, "x"),
            0,
            "single-line body should report 0 body lines"
        );
    }

    #[test]
    fn count_body_lines_one_for_three_line_shape() {
        // The standard formatter shape:
        //   fn x() -> u32 {
        //       0
        //   }
        // Exactly one line strictly between the braces.
        let content = "fn x() -> u32 {\n    0\n}\n";
        let mut fp = make_fingerprint("src/x.rs", &["x"], &[]);
        fp.content = content.to_string();

        assert_eq!(
            count_body_lines(&fp, "x"),
            1,
            "three-line shape should report 1 body line"
        );
    }

    #[test]
    fn count_body_lines_counts_actual_body_statements() {
        // Multi-line body with 4 statements between the braces.
        let content = "fn process(items: &[Item]) -> Result {\n    let mut out = Vec::new();\n    for item in items {\n        out.push(item.clone());\n    }\n    Ok(out)\n}\n";
        let mut fp = make_fingerprint("src/process.rs", &["process"], &[]);
        fp.content = content.to_string();

        // Lines strictly between `{` and `}`:
        //   let mut out = Vec::new();
        //   for item in items {
        //       out.push(item.clone());
        //   }
        //   Ok(out)
        // → 5 body lines.
        assert_eq!(
            count_body_lines(&fp, "process"),
            5,
            "should count actual body lines (5), not the wrapping span (7)"
        );
    }

    #[test]
    fn near_duplicate_skips_idiomatic_collection_methods() {
        // The triggering case for #1517: every Vec/HashMap wrapper in the
        // ecosystem defines `fn len(&self) -> usize { self.inner.len() }`,
        // and Clippy's `len_without_is_empty` lint requires `is_empty`
        // alongside it. Two structs each defining both methods must NOT
        // produce near_duplicate findings.
        let content_a = "struct A { inner: Vec<u8> }\nimpl A {\n    pub fn len(&self) -> usize {\n        self.inner.len()\n    }\n    pub fn is_empty(&self) -> bool {\n        self.inner.is_empty()\n    }\n}\n";
        let content_b = "struct B { inner: HashMap<String, u32> }\nimpl B {\n    pub fn len(&self) -> usize {\n        self.inner.len()\n    }\n    pub fn is_empty(&self) -> bool {\n        self.inner.is_empty()\n    }\n}\n";

        let fp1 = make_fp_with_content(
            "src/core/a.rs",
            content_a,
            &[("len", "hash_a_len"), ("is_empty", "hash_a_emp")],
            &[("len", "SAME_LEN"), ("is_empty", "SAME_EMP")],
        );
        let fp2 = make_fp_with_content(
            "src/core/b.rs",
            content_b,
            &[("len", "hash_b_len"), ("is_empty", "hash_b_emp")],
            &[("len", "SAME_LEN"), ("is_empty", "SAME_EMP")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert!(
            findings.is_empty(),
            "idiomatic collection-wrapper methods (`len`, `is_empty`) must not be flagged as near-duplicates; got {} finding(s): {:?}",
            findings.len(),
            findings.iter().map(|f| &f.description).collect::<Vec<_>>(),
        );
    }

    #[test]
    fn near_duplicate_still_flags_real_duplicates() {
        // Regression guard against over-suppressing: a non-trivially-named
        // method with identical structural hash but different body hashes
        // across two files (and a 3+ body-line shape) MUST still be flagged.
        let content_a = "fn compute_fixability(item: &Item) -> bool {\n    let score = item.score();\n    let threshold = THRESHOLD_A;\n    score > threshold\n}\n";
        let content_b = "fn compute_fixability(item: &Item) -> bool {\n    let score = item.score();\n    let threshold = THRESHOLD_B;\n    score > threshold\n}\n";

        let fp1 = make_fp_with_content(
            "src/core/a.rs",
            content_a,
            &[("compute_fixability", "hash_a")],
            &[("compute_fixability", "SAME_STRUCT")],
        );
        let fp2 = make_fp_with_content(
            "src/core/b.rs",
            content_b,
            &[("compute_fixability", "hash_b")],
            &[("compute_fixability", "SAME_STRUCT")],
        );

        let findings = detect_near_duplicates(&[&fp1, &fp2]);
        assert_eq!(
            findings.len(),
            2,
            "real near-duplicates (non-idiomatic name, multi-line body, distinct body hashes) must still be flagged",
        );
        assert!(findings
            .iter()
            .all(|f| f.description.contains("compute_fixability")));
    }

    // ========================================================================
    // Intra-method duplication tests
    // ========================================================================

    #[test]
    fn intra_method_detects_duplicated_block() {
        // Simulate a merge artifact: same 5-line block appears twice
        let content = "<?php\nclass PipelineSteps {\n    public function handle_update( $request ) {\n        $config = array();\n        $has_provider = $request->has_param( 'provider' );\n        $has_model = $request->has_param( 'model' );\n        $has_prompt = $request->has_param( 'system_prompt' );\n        $has_disabled = $request->has_param( 'disabled_tools' );\n        $has_key = $request->has_param( 'ai_api_key' );\n\n        if ( $has_provider ) {\n            $config['provider'] = sanitize_text_field( $request->get_param( 'provider' ) );\n        }\n\n        $has_provider = $request->has_param( 'provider' );\n        $has_model = $request->has_param( 'model' );\n        $has_prompt = $request->has_param( 'system_prompt' );\n        $has_disabled = $request->has_param( 'disabled_tools' );\n        $has_key = $request->has_param( 'ai_api_key' );\n\n        if ( $has_provider ) {\n            $config['provider'] = sanitize_text_field( $request->get_param( 'provider' ) );\n        }\n\n        return $config;\n    }\n}\n";

        let mut fp = make_fingerprint(
            "inc/Api/Pipelines/PipelineSteps.php",
            &["handle_update"],
            &[],
        );
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);

        assert!(
            !findings.is_empty(),
            "Should detect duplicated block within handle_update"
        );
        assert!(findings[0].kind == AuditFinding::IntraMethodDuplicate);
        assert!(findings[0].description.contains("handle_update"));
    }

    #[test]
    fn intra_method_no_false_positive_on_unique_code() {
        let content = "<?php\nclass Handler {\n    public function process( $data ) {\n        $name = sanitize_text_field( $data['name'] );\n        $email = sanitize_email( $data['email'] );\n        $phone = sanitize_text_field( $data['phone'] );\n        $address = sanitize_text_field( $data['address'] );\n        $city = sanitize_text_field( $data['city'] );\n\n        $result = $this->save( $name, $email );\n        $this->notify( $result );\n        $this->log_action( $result );\n        $this->update_cache( $result );\n        $this->send_confirmation( $email );\n\n        return $result;\n    }\n}\n";

        let mut fp = make_fingerprint("inc/Handler.php", &["process"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            findings.is_empty(),
            "Unique code should not trigger intra-method duplication"
        );
    }

    #[test]
    fn intra_method_skips_short_methods() {
        let content = "fn short() {\n    let a = 1;\n    let b = 2;\n    let c = a + b;\n    println!(\"{}\", c);\n}\n";

        let mut fp = make_fingerprint("src/short.rs", &["short"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(findings.is_empty(), "Short methods should be skipped");
    }

    #[test]
    fn intra_method_rust_function_duplicated_block() {
        let content = "fn process_items(items: &[Item]) -> Vec<Result> {\n    let mut results = Vec::new();\n    let config = load_config();\n    let validator = Validator::new(&config);\n    let processor = Processor::new(&config);\n    let output = processor.run(&items[0]);\n\n    results.push(output);\n\n    let config = load_config();\n    let validator = Validator::new(&config);\n    let processor = Processor::new(&config);\n    let output = processor.run(&items[0]);\n\n    results.push(output);\n\n    results\n}\n";

        let mut fp = make_fingerprint("src/core/pipeline.rs", &["process_items"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            !findings.is_empty(),
            "Should detect duplicated block in Rust function"
        );
    }

    #[test]
    fn intra_method_ignores_match_arm_tail_scaffolding() {
        // Sibling dispatch arms in a `run_*` match share a boilerplate tail:
        //   )?;
        //   Ok((Variant(output), 0))
        //   }
        //   OtherArm::Name { ... } => {
        //
        // After normalization these look like 5+ identical lines across arms,
        // but they're Rust syntax, not duplicated logic. The scaffolding
        // filter should suppress the finding.
        //
        // Each arm body here is intentionally one unique line plus the
        // scaffolding tail — so the only thing that repeats is scaffolding.
        let content = "\
fn run_pr(args: PrArgs) -> Result {
    match args.command {
        PrCommand::Create {
            comp_create,
        } => {
            do_create_thing(comp_create);
            Ok((GitCommandOutput::Pr(output), 0))
        }
        PrCommand::Edit {
            comp_edit,
        } => {
            do_edit_thing(comp_edit);
            Ok((GitCommandOutput::Pr(output), 0))
        }
        PrCommand::Comment {
            comp_comment,
        } => {
            do_comment_thing(comp_comment);
            Ok((GitCommandOutput::Pr(output), 0))
        }
    }
}
";
        let mut fp = make_fingerprint("src/commands/git.rs", &["run_pr"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            findings.is_empty(),
            "Match-arm tail scaffolding should not be flagged as duplication; got {} finding(s): {:?}",
            findings.len(),
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn intra_method_still_flags_real_duplication_with_scaffolding_tails() {
        // If the repeated block contains real logic (a `let` + a call that
        // isn't an Ok/Err wrapper), we should still flag it even when it's
        // surrounded by structural lines.
        let content = "\
fn process_twice() -> Result {
    let items = load_items()?;
    let validator = Validator::new();
    let processor = Processor::new();
    let output = processor.run(&items);
    save_output(&output)?;

    let items = load_items()?;
    let validator = Validator::new();
    let processor = Processor::new();
    let output = processor.run(&items);
    save_output(&output)?;

    Ok(())
}
";
        let mut fp = make_fingerprint("src/core/pipeline.rs", &["process_twice"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            !findings.is_empty(),
            "Real duplication with logic lines should still be detected"
        );
    }

    #[test]
    fn intra_method_ignores_complementary_output_dto_tails() {
        let content = r#"
fn show(builtin: bool) -> CmdResult<ConfigOutput> {
    if builtin {
        Ok((
            ConfigOutput {
                command: "config.show".to_string(),
                defaults: Some(defaults::builtin_defaults()),
                config: None,
                path: None,
                exists: None,
                pointer: None,
                value: None,
                deleted: None,
            },
            0,
        ))
    } else {
        let config = defaults::load_config();
        Ok((
            ConfigOutput {
                command: "config.show".to_string(),
                config: Some(config),
                defaults: None,
                path: None,
                exists: None,
                pointer: None,
                value: None,
                deleted: None,
            },
            0,
        ))
    }
}
"#;
        let mut fp = make_fingerprint("src/commands/config.rs", &["show"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            findings.is_empty(),
            "Complementary DTO literal tails should not be flagged: {:?}",
            findings
                .iter()
                .map(|f| f.description.as_str())
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn intra_method_ignores_repeated_error_envelopes() {
        let content = r#"
fn write_file_atomic(path: &Path, content: &str, operation: &str) -> Result<()> {
    let parent = path.parent().ok_or_else(|| {
        Error::internal_io(
            format!("Invalid path: {}", path.display()),
            Some(operation.to_string()),
        )
    })?;

    let filename = path.file_name().ok_or_else(|| {
        Error::internal_io(
            format!("Invalid path: {}", path.display()),
            Some(operation.to_string()),
        )
    })?;

    let tmp_path = parent.join(format!("{}.tmp", filename.to_string_lossy()));
    write_tmp(tmp_path, content)
}
"#;
        let mut fp = make_fingerprint(
            "src/core/engine/local_files.rs",
            &["write_file_atomic"],
            &[],
        );
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            findings.is_empty(),
            "Repeated error envelopes should not be flagged: {:?}",
            findings
                .iter()
                .map(|f| f.description.as_str())
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn intra_method_ignores_short_sibling_branch_repetition() {
        let content = r#"
fn resolve_effective_glob(args: &Args, component: &Component) -> Result<Option<String>> {
    if args.changed_only {
        let changed_files = git::working_tree_changes(&component.local_path)?;
        if changed_files.is_empty() {
            println!("No files in working tree changes");
            return Ok(Some(String::new()));
        }

        let abs_files: Vec<String> = changed_files
            .iter()
            .map(|f| format!("{}/{}", component.local_path, f))
            .collect();

        if abs_files.len() == 1 {
            Ok(Some(abs_files[0].clone()))
        } else {
            Ok(Some(format!("{{{}}}", abs_files.join(","))))
        }
    } else if let Some(ref git_ref) = args.changed_since {
        let changed_files = git::get_files_changed_since(&component.local_path, git_ref)?;
        if changed_files.is_empty() {
            println!("No files changed since {}", git_ref);
            return Ok(Some(String::new()));
        }

        let abs_files: Vec<String> = changed_files
            .iter()
            .map(|f| format!("{}/{}", component.local_path, f))
            .collect();

        if abs_files.len() == 1 {
            Ok(Some(abs_files[0].clone()))
        } else {
            Ok(Some(format!("{{{}}}", abs_files.join(","))))
        }
    } else {
        Ok(args.glob.clone())
    }
}
"#;
        let mut fp = make_fingerprint(
            "src/core/extension/lint/run.rs",
            &["resolve_effective_glob"],
            &[],
        );
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            findings.is_empty(),
            "Short sibling-branch repetition should not be flagged: {:?}",
            findings
                .iter()
                .map(|f| f.description.as_str())
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn intra_method_ignores_repeated_multiline_call_argument_tails() {
        let content = r#"
fn env(extension: &Extension, local_path: &Path) -> Result<()> {
    if let Some(detected) = run_component_env_detector(extension, local_path)? {
        apply_component_env_detector_output(
            detected,
            &mut node_version,
            &mut node_source,
            &mut php_version,
            &mut php_source,
        );
    }

    if let Some(runtime) = extension.runtime.as_ref() {
        apply_extension_runtime_requirements(
            ext_id,
            runtime,
            &mut node_version,
            &mut node_source,
            &mut php_version,
            &mut php_source,
        );
    }
}
"#;
        let mut fp = make_fingerprint("src/commands/component.rs", &["env"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            findings.is_empty(),
            "Repeated argument tails on different calls should not be flagged: {:?}",
            findings
                .iter()
                .map(|f| f.description.as_str())
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn intra_method_ignores_repeated_match_arm_result_shapes() {
        let content = r#"
fn search(mode: SearchMode, line: &str, term: &str) {
    match mode {
        SearchMode::Boundary => {
            for pos in find_boundary_matches(line, term) {
                results.push(Match {
                    file: relative.clone(),
                    line: line_num + 1,
                    column: pos + 1,
                    matched: term.to_string(),
                    context: line.to_string(),
                });
            }
        }
        SearchMode::Literal => {
            for pos in find_literal_matches(line, term) {
                results.push(Match {
                    file: relative.clone(),
                    line: line_num + 1,
                    column: pos + 1,
                    matched: term.to_string(),
                    context: line.to_string(),
                });
            }
        }
    }
}
"#;
        let mut fp = make_fingerprint("src/core/engine/codebase_scan.rs", &["search"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            findings.is_empty(),
            "Repeated match-arm result shapes should not be flagged: {:?}",
            findings
                .iter()
                .map(|f| f.description.as_str())
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn intra_method_still_flags_adjacent_logic_copy_paste() {
        let content = r#"
fn rebuild_twice(items: &[Item]) -> Result<()> {
    let config = load_config()?;
    let validator = Validator::new(&config);
    let processor = Processor::new(&config);
    let output = processor.run(&items[0]);
    save_output(&output)?;

    let config = load_config()?;
    let validator = Validator::new(&config);
    let processor = Processor::new(&config);
    let output = processor.run(&items[0]);
    save_output(&output)?;

    Ok(())
}
"#;
        let mut fp = make_fingerprint("src/core/pipeline.rs", &["rebuild_twice"], &[]);
        fp.content = content.to_string();

        let findings = detect_intra_method_duplicates(&[&fp]);
        assert!(
            !findings.is_empty(),
            "Adjacent repeated logic should still be reported"
        );
    }

    #[test]
    fn scaffolding_line_classifier() {
        // Positive cases (structural).
        for line in &[
            "}",
            "},",
            ")",
            ")?;",
            "))",
            "))?",
            "path,",
            "component_id,",
            "path",
            "ok((gitcommandoutput::pr(output), 0))",
            "ok(output)",
            "err(e)",
            "none",
            "} => {",
            "_ => {",
            "foo => {",
        ] {
            assert!(
                is_scaffolding_line(line),
                "Expected scaffolding: {:?}",
                line
            );
        }

        // Negative cases (real logic).
        for line in &[
            "let x = foo();",
            "x = y + 1",
            "if x.is_empty() {",
            "for item in items {",
            "compute(&items)?",
            ".stdout(std::process::stdio::null())",
        ] {
            assert!(
                !is_scaffolding_line(line) || has_logic_signal(line),
                "Expected logic: {:?}",
                line
            );
        }
    }

    #[test]
    fn logic_signal_detector() {
        assert!(has_logic_signal("let x = foo();"));
        assert!(has_logic_signal("x = 1"));
        assert!(has_logic_signal("if cond {"));
        assert!(has_logic_signal("for x in y {"));
        assert!(has_logic_signal("while true {"));
        assert!(has_logic_signal("match thing {"));
        assert!(has_logic_signal("return x"));
        assert!(has_logic_signal(".stdout(something())"));
        assert!(has_logic_signal("compute(&items)"));

        // Return wrappers are NOT logic (they're structural tail expressions).
        assert!(!has_logic_signal("ok(())"));
        assert!(!has_logic_signal("ok((output, 0))"));
        assert!(!has_logic_signal("err(e)"));
        assert!(!has_logic_signal("some(x)"));
        assert!(!has_logic_signal("none"));

        // Pure punctuation is not logic.
        assert!(!has_logic_signal("}"));
        assert!(!has_logic_signal(")?;"));
    }

    #[test]
    fn find_method_body_php() {
        let content =
            "<?php\nclass Foo {\n    public function bar() {\n        return 1;\n    }\n}\n";
        let lines: Vec<&str> = content.lines().collect();
        let result = find_method_body(&lines, "bar");
        assert!(result.is_some());
        let (open, close) = result.unwrap();
        assert!(lines[open].contains('{'));
        assert!(lines[close].contains('}'));
    }

    #[test]
    fn find_method_body_rust() {
        let content = "fn hello() {\n    println!(\"hi\");\n}\n";
        let lines: Vec<&str> = content.lines().collect();
        let result = find_method_body(&lines, "hello");
        assert!(result.is_some());
    }

    #[test]
    fn find_method_body_missing() {
        let content = "fn other() {\n    println!(\"hi\");\n}\n";
        let lines: Vec<&str> = content.lines().collect();
        let result = find_method_body(&lines, "nonexistent");
        assert!(result.is_none());
    }

    // ========================================================================
    // Parallel Implementation Detection tests
    // ========================================================================

    fn make_fingerprint_with_content(
        path: &str,
        methods: &[&str],
        content: &str,
    ) -> FileFingerprint {
        FileFingerprint {
            relative_path: path.to_string(),
            language: Language::Rust,
            methods: methods.iter().map(|s| s.to_string()).collect(),
            content: content.to_string(),
            ..Default::default()
        }
    }

    #[test]
    fn detects_parallel_implementation() {
        // Both bodies loop over a worklist — Looping ↔ Looping matches at the
        // standard Jaccard floor. Mirrors the real `copy_dir_recursive` ↔
        // `copy_directory` shape from issue #2334.
        let fp1 = make_fingerprint_with_content(
            "src/deploy.rs",
            &["deploy_to_server"],
            "fn deploy_to_server() {\n    for host in hosts {\n        validate_component();\n        build_artifact();\n        upload_to_host();\n        run_post_hooks();\n        notify_complete();\n    }\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/upgrade.rs",
            &["upgrade_on_server"],
            "fn upgrade_on_server() {\n    for host in hosts {\n        validate_component();\n        build_artifact();\n        upload_to_host();\n        run_post_hooks();\n        send_notification();\n    }\n}",
        );

        let findings = detect_parallel_implementations(
            &[&fp1, &fp2],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert_eq!(findings.len(), 2, "Should emit one finding per file");
        assert!(findings
            .iter()
            .all(|f| f.kind == AuditFinding::ParallelImplementation));
        assert!(findings.iter().any(|f| f.file == "src/deploy.rs"));
        assert!(findings.iter().any(|f| f.file == "src/upgrade.rs"));
    }

    #[test]
    fn no_parallel_for_unrelated_functions() {
        let fp1 = make_fingerprint_with_content(
            "src/deploy.rs",
            &["deploy_to_server"],
            "fn deploy_to_server() {\n    validate();\n    build();\n    upload();\n    notify();\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/parser.rs",
            &["parse_config"],
            "fn parse_config() {\n    read_file();\n    tokenize();\n    parse_ast();\n    validate_schema();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&fp1, &fp2],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );
        assert!(
            findings.is_empty(),
            "Completely different call sets should not flag"
        );
    }

    #[test]
    fn no_parallel_for_same_file() {
        let fp = make_fingerprint_with_content(
            "src/ops.rs",
            &["deploy_op", "upgrade_op"],
            "fn deploy_op() {\n    validate();\n    build();\n    upload();\n    notify();\n}\nfn upgrade_op() {\n    validate();\n    build();\n    upload();\n    notify();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&fp],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );
        assert!(
            findings.is_empty(),
            "Same-file methods should not be flagged as parallel"
        );
    }

    #[test]
    fn no_parallel_for_trivial_methods() {
        let fp1 = make_fingerprint_with_content(
            "src/a.rs",
            &["small_a"],
            "fn small_a() {\n    foo();\n    bar();\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/b.rs",
            &["small_b"],
            "fn small_b() {\n    foo();\n    bar();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&fp1, &fp2],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );
        assert!(
            findings.is_empty(),
            "Methods with < MIN_CALL_COUNT calls should be skipped"
        );
    }

    #[test]
    fn no_parallel_for_plumbing_only_call_patterns() {
        let fs_helper = make_fingerprint_with_content(
            "src/files.rs",
            &["plugin_header_version"],
            "fn plugin_header_version() {\n    path();\n    read_dir();\n    to_str();\n    success();\n}",
        );
        let extension_scan = make_fingerprint_with_content(
            "src/extensions.rs",
            &["scan_available_extensions"],
            "fn scan_available_extensions() {\n    path();\n    read_dir();\n    to_str();\n    is_dir();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&fs_helper, &extension_scan],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert!(
            findings.is_empty(),
            "Plumbing-only filesystem call overlap should not flag"
        );
    }

    #[test]
    fn no_parallel_for_command_wrapper_plumbing() {
        let command_runner = make_fingerprint_with_content(
            "src/command.rs",
            &["succeeded_in"],
            "fn succeeded_in() {\n    args();\n    current_dir();\n    output();\n    success();\n}",
        );
        let branch_reader = make_fingerprint_with_content(
            "src/stack.rs",
            &["current_branch"],
            "fn current_branch() {\n    args();\n    current_dir();\n    output();\n    success();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&command_runner, &branch_reader],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert!(
            findings.is_empty(),
            "Shared Command setup/result checks are plumbing, not a workflow"
        );
    }

    #[test]
    fn no_parallel_for_text_parsing_plumbing() {
        let http_handler = make_fingerprint_with_content(
            "src/core/daemon.rs",
            &["handle_connection"],
            "fn handle_connection() {\n    request.lines().next().split_whitespace();\n    route();\n    write_response();\n}",
        );
        let process_probe = make_fingerprint_with_content(
            "src/core/server/client.rs",
            &["probe_child_resources"],
            "fn probe_child_resources() {\n    stdout.lines().next().split_whitespace();\n    parse_rss();\n    parse_cpu();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&http_handler, &process_probe],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert!(
            findings.is_empty(),
            "Shared line tokenization is parsing plumbing, not a reusable workflow"
        );
    }

    #[test]
    fn no_parallel_for_deploy_plumbing_only_patterns() {
        let artifact_deploy = make_fingerprint_with_content(
            "src/core/deploy/safety_and_artifact.rs",
            &["deploy_artifact"],
            "fn deploy_artifact() {\n    quote_path();\n    execute();\n    failure();\n    render_map();\n    fix_deployed_permissions();\n}",
        );
        let override_deploy = make_fingerprint_with_content(
            "src/core/deploy/version_overrides.rs",
            &["deploy_with_override"],
            "fn deploy_with_override() {\n    quote_path();\n    execute();\n    failure();\n    render_map();\n    fix_deployed_permissions();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&artifact_deploy, &override_deploy],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert!(
            findings.is_empty(),
            "Shared SSH/deploy epilogue calls should not imply an extractable workflow"
        );
    }

    #[test]
    fn detects_parallel_implementation_after_plumbing_filter() {
        // Both bodies loop over their PR list — Looping ↔ Looping clears the
        // body-shape gate at the standard Jaccard floor.
        let apply = make_fingerprint_with_content(
            "src/core/stack/apply.rs",
            &["apply_stack"],
            "fn apply_stack() {\n    for pr in prs {\n        ensure_head_remote();\n        checkout_force();\n        fetch_pr_meta();\n        cherry_pick();\n        record_applied_pr();\n        run_git();\n        success();\n    }\n}",
        );
        let sync = make_fingerprint_with_content(
            "src/core/stack/sync.rs",
            &["sync_stack"],
            "fn sync_stack() {\n    for pr in prs {\n        ensure_head_remote();\n        checkout_force();\n        fetch_pr_meta();\n        cherry_pick();\n        record_synced_pr();\n        run_git();\n        success();\n    }\n}",
        );

        let findings = detect_parallel_implementations(
            &[&apply, &sync],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert_eq!(
            findings.len(),
            2,
            "Domain-heavy stack pairs should still flag"
        );
        assert!(findings
            .iter()
            .any(|finding| finding.description.contains("`ensure_head_remote`")));
        assert!(findings
            .iter()
            .all(|finding| !finding.description.contains("`run_git`")));
    }

    #[test]
    fn no_parallel_for_generic_names() {
        // "run" is in GENERIC_NAMES
        let fp1 = make_fingerprint_with_content(
            "src/a.rs",
            &["run"],
            "fn run() {\n    validate();\n    build();\n    upload();\n    notify();\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/b.rs",
            &["execute"],
            "fn execute() {\n    validate();\n    build();\n    upload();\n    notify();\n}",
        );

        // "run" is skipped, so only one method in the pool — no pair to compare
        let findings = detect_parallel_implementations(
            &[&fp1, &fp2],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );
        // Only fp2's "execute" has a valid call sequence; fp1's "run" is filtered
        // So there's only 1 candidate, no pair → no findings
        assert!(findings.is_empty(), "Generic names should be filtered out");
    }

    #[test]
    fn extract_calls_skips_keywords() {
        let body = "if something() {\n    let x = process();\n    for item in list() {\n        handle(item);\n    }\n}";
        let calls = extract_calls_from_body(body, &std::collections::HashSet::new());
        assert!(calls.contains(&"something".to_string()));
        assert!(calls.contains(&"process".to_string()));
        assert!(calls.contains(&"list".to_string()));
        assert!(calls.contains(&"handle".to_string()));
        assert!(!calls.contains(&"if".to_string()));
        assert!(!calls.contains(&"for".to_string()));
        assert!(!calls.contains(&"let".to_string()));
    }

    #[test]
    fn jaccard_identical_sets() {
        let a = vec!["foo".to_string(), "bar".to_string()];
        assert!((jaccard_similarity(&a, &a) - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn jaccard_disjoint_sets() {
        let a = vec!["foo".to_string()];
        let b = vec!["bar".to_string()];
        assert!((jaccard_similarity(&a, &b)).abs() < f64::EPSILON);
    }

    #[test]
    fn lcs_identical_sequences() {
        let a = vec!["a".to_string(), "b".to_string(), "c".to_string()];
        assert_eq!(lcs_length(&a, &a), 3);
        assert!((lcs_ratio(&a, &a) - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn lcs_partial_overlap() {
        let a = vec!["a".to_string(), "b".to_string(), "c".to_string()];
        let b = vec!["a".to_string(), "x".to_string(), "c".to_string()];
        assert_eq!(lcs_length(&a, &b), 2); // a, c
    }

    #[test]
    fn convention_methods_skip_parallel_detection() {
        // Two methods with identical call patterns — would normally flag.
        // Wrapped in a loop so they clear the body-shape gate.
        let fp1 = make_fingerprint_with_content(
            "src/deploy.rs",
            &["registerAbilities"],
            "fn registerAbilities() {\n    for ability in abilities {\n        validate_component();\n        build_artifact();\n        upload_to_host();\n        run_post_hooks();\n        notify_complete();\n    }\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/upgrade.rs",
            &["registerAbility"],
            "fn registerAbility() {\n    for ability in abilities {\n        validate_component();\n        build_artifact();\n        upload_to_host();\n        run_post_hooks();\n        send_notification();\n    }\n}",
        );

        // Without convention methods: flagged
        let findings = detect_parallel_implementations(
            &[&fp1, &fp2],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );
        assert_eq!(findings.len(), 2, "Should flag without convention context");

        // With EITHER method as convention-expected: NOT flagged
        let conv_methods: std::collections::HashSet<String> = ["registerAbilities"] // only one of the two
            .iter()
            .map(|s| s.to_string())
            .collect();
        let findings = detect_parallel_implementations(
            &[&fp1, &fp2],
            &conv_methods,
            &DuplicationDetectorConfig::default(),
        );
        assert!(
            findings.is_empty(),
            "Pairs involving convention methods should not be flagged, got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    // ========================================================================
    // Body-shape gate tests (issue #2334)
    // ========================================================================

    #[test]
    fn body_shape_gate_two_loops_with_shared_calls_flag() {
        // Mirrors the real `copy_dir_recursive` ↔ `copy_directory` shape that
        // we MUST keep flagging after the body-shape gate ships.
        let copy_dir_recursive = make_fingerprint_with_content(
            "src/core/extension/lifecycle.rs",
            &["copy_dir_recursive"],
            "fn copy_dir_recursive() {\n    create_dir_all(dst);\n    for entry in read_dir(src) {\n        copy_file_entry();\n        record_copied();\n        verify_target();\n    }\n}",
        );
        let copy_directory = make_fingerprint_with_content(
            "src/core/engine/invocation.rs",
            &["copy_directory"],
            "fn copy_directory() {\n    create_dir_all(dst);\n    for entry in read_dir(src) {\n        copy_file_entry();\n        record_copied();\n        preserve_artifact();\n    }\n}",
        );

        let findings = detect_parallel_implementations(
            &[&copy_dir_recursive, &copy_directory],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert_eq!(
            findings.len(),
            2,
            "Two looping copy helpers with shared calls must still flag — that is the real finding from #2334"
        );
    }

    #[test]
    fn body_shape_gate_kills_single_file_vs_recursive_walk_fp() {
        // The canonical FP from issue #2334:
        // `copy_artifact_file` is StraightLine (single `fs::copy` after a
        // `create_dir_all` of the parent), `copy_dir_recursive` is
        // Looping+Recursive (recursive walk over `read_dir`). They share
        // `create_dir_all` and `copy` but the workflows are not the same.
        let copy_artifact_file = make_fingerprint_with_content(
            "src/core/observation/store.rs",
            &["copy_artifact_file"],
            "fn copy_artifact_file() {\n    let parent = target_parent();\n    create_dir_all(parent);\n    copy(source, target);\n    verify_size();\n    record_copy();\n}",
        );
        let copy_dir_recursive = make_fingerprint_with_content(
            "src/core/extension/lifecycle.rs",
            &["copy_dir_recursive"],
            "fn copy_dir_recursive() {\n    create_dir_all(dst);\n    for entry in read_dir(src) {\n        copy(entry, target);\n        verify_size();\n        record_copy();\n        copy_dir_recursive(entry, dst);\n    }\n}",
        );

        let findings = detect_parallel_implementations(
            &[&copy_artifact_file, &copy_dir_recursive],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert!(
            findings.is_empty(),
            "Single-file copy (StraightLine) vs recursive walk (Looping) must not flag — got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn body_shape_gate_two_straight_line_below_raised_jaccard_floor() {
        // Two StraightLine bodies: 4 shared calls, 6 union → Jaccard 0.667.
        // Below the raised StraightLine floor of 0.7 → must NOT flag.
        let helper_a = make_fingerprint_with_content(
            "src/core/a.rs",
            &["build_thing_a"],
            "fn build_thing_a() {\n    let x = open_resource();\n    register_handler();\n    configure_options();\n    finalize_build();\n    emit_metric_a();\n}",
        );
        let helper_b = make_fingerprint_with_content(
            "src/core/b.rs",
            &["build_thing_b"],
            "fn build_thing_b() {\n    let x = open_resource();\n    register_handler();\n    configure_options();\n    finalize_build();\n    emit_metric_b();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&helper_a, &helper_b],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert!(
            findings.is_empty(),
            "Two StraightLine bodies at Jaccard 0.667 must not flag under the raised floor — got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn body_shape_gate_two_straight_line_above_raised_jaccard_floor() {
        // Same StraightLine pair but with identical signal calls (Jaccard 1.0)
        // — clears the raised floor and MUST flag. This proves the gate is a
        // shape filter, not a blanket ban on StraightLine pairs.
        let helper_a = make_fingerprint_with_content(
            "src/core/a.rs",
            &["build_thing_a"],
            "fn build_thing_a() {\n    let x = open_resource();\n    register_handler();\n    configure_options();\n    finalize_build();\n    emit_metric();\n}",
        );
        let helper_b = make_fingerprint_with_content(
            "src/core/b.rs",
            &["build_thing_b"],
            "fn build_thing_b() {\n    let x = open_resource();\n    register_handler();\n    configure_options();\n    finalize_build();\n    emit_metric();\n}",
        );

        let findings = detect_parallel_implementations(
            &[&helper_a, &helper_b],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert_eq!(
            findings.len(),
            2,
            "Two StraightLine bodies above the raised Jaccard floor must still flag"
        );
    }

    #[test]
    fn body_shape_gate_recursive_to_recursive_flags() {
        // Two recursive helpers (no loop, but each calls itself) share the
        // same workflow — Recursive ↔ Recursive is compatible and uses the
        // standard Jaccard floor.
        let walk_a = make_fingerprint_with_content(
            "src/core/a.rs",
            &["walk_tree_a"],
            "fn walk_tree_a(node) {\n    visit_node();\n    record_step();\n    sanitize_value();\n    log_progress();\n    walk_tree_a(child);\n}",
        );
        let walk_b = make_fingerprint_with_content(
            "src/core/b.rs",
            &["walk_tree_b"],
            "fn walk_tree_b(node) {\n    visit_node();\n    record_step();\n    sanitize_value();\n    log_progress();\n    walk_tree_b(child);\n}",
        );

        let findings = detect_parallel_implementations(
            &[&walk_a, &walk_b],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert_eq!(
            findings.len(),
            2,
            "Recursive ↔ Recursive helpers with shared call set must flag"
        );
    }

    // ========================================================================
    // Extension-supplied trivial/plumbing call list tests (#2333)
    // ========================================================================

    #[test]
    fn extension_trivial_calls_filter_out_of_signal() {
        // Two parallel deploy/upgrade workflows that share several
        // domain-meaningful calls — flagged by default. With `custom_helper`
        // declared trivial via extension, that call is filtered out of the
        // recorded sequence; the remaining shared signal is unchanged so
        // this test specifically demonstrates the trivial path is wired
        // (compare against the default-config sanity below).
        let fp1 = make_fingerprint_with_content(
            "src/deploy.rs",
            &["deploy_to_server"],
            "fn deploy_to_server() {\n    custom_helper();\n    validate_component();\n    build_artifact();\n    upload_to_host();\n    run_post_hooks();\n    notify_complete();\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/upgrade.rs",
            &["upgrade_on_server"],
            "fn upgrade_on_server() {\n    custom_helper();\n    validate_component();\n    build_artifact();\n    upload_to_host();\n    run_post_hooks();\n    send_notification();\n}",
        );

        // Default config: flagged (has the shared workflow signal).
        let default_findings = detect_parallel_implementations(
            &[&fp1, &fp2],
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );
        assert!(
            !default_findings.is_empty(),
            "Sanity: workflow pair must flag without extension config"
        );
        assert!(
            default_findings
                .iter()
                .any(|f| f.description.contains("`custom_helper`")),
            "Sanity: without trivial-list filtering, `custom_helper` should appear in the shared-call summary"
        );

        // Extension-supplied trivial removes `custom_helper` from sequences.
        let cfg = DuplicationDetectorConfig {
            trivial_calls: vec!["custom_helper".to_string()],
            plumbing_calls: vec![],
        };
        let findings =
            detect_parallel_implementations(&[&fp1, &fp2], &std::collections::HashSet::new(), &cfg);
        // The pair still flags (other workflow signal remains), but the
        // extension-trivial name is gone from the description.
        assert!(
            findings
                .iter()
                .all(|f| !f.description.contains("`custom_helper`")),
            "Extension-trivial `custom_helper` must be filtered out of the call sequence and shared-call summary, got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn body_shape_detection_smoke() {
        assert_eq!(
            detect_body_shape("    foo();\n    bar();\n", "thing"),
            BodyShape::StraightLine
        );
        assert_eq!(
            detect_body_shape("    for entry in items {\n        foo();\n    }\n", "thing"),
            BodyShape::Looping
        );
        assert_eq!(
            detect_body_shape("    items.iter().map(|x| x).collect();\n", "thing"),
            BodyShape::Looping
        );
        assert_eq!(
            detect_body_shape("    foo();\n    walk(child);\n", "walk"),
            BodyShape::Recursive
        );
        // Identifier guard — `redo_walk` must not register as a self-call to `walk`.
        assert_eq!(
            detect_body_shape("    redo_walk(x);\n", "walk"),
            BodyShape::StraightLine
        );
        // `format!` contains the substring `for` but must not register as Looping.
        assert_eq!(
            detect_body_shape("    let s = format!(\"x\");\n    foo();\n", "thing"),
            BodyShape::StraightLine
        );
    }

    #[test]
    fn extension_plumbing_calls_filter_out_of_signal() {
        // Two methods share only `log_event` as workflow overlap — everything
        // else is unique. With default config, `log_event` is workflow signal
        // and the pair flags. With `log_event` declared plumbing via extension,
        // the shared signal collapses below MIN_CALL_COUNT and the pair is
        // dropped.
        let fp1 = make_fingerprint_with_content(
            "src/a.rs",
            &["worker_a"],
            "fn worker_a() {\n    log_event();\n    log_event();\n    log_event();\n    log_event();\n    step_a1();\n    step_a2();\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/b.rs",
            &["worker_b"],
            "fn worker_b() {\n    log_event();\n    log_event();\n    log_event();\n    log_event();\n    step_b1();\n    step_b2();\n}",
        );

        // Without extension config, log_event is recorded as signal — but with
        // the existing built-in idiomatic floors, results vary. We only assert
        // the extension hook genuinely silences any pairing.
        let cfg = DuplicationDetectorConfig {
            trivial_calls: vec![],
            plumbing_calls: vec!["log_event".to_string()],
        };
        let findings =
            detect_parallel_implementations(&[&fp1, &fp2], &std::collections::HashSet::new(), &cfg);
        assert!(
            findings
                .iter()
                .all(|f| !f.description.contains("`log_event`")),
            "Extension-plumbing `log_event` must be removed from signal calls / shared-call summary, got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn extension_call_lists_fix_env_path_helper_fp_2333() {
        // Direct regression for issue #2333: `cache_fallback_root` ↔ `homeboy_data`
        // both call `var`, `cfg`, `not`, `internal_unexpected` (Rust env-derived
        // path plumbing). Without extension hints the detector flags them; with
        // the rust manifest declaring those calls as trivial/plumbing, no FP.
        let cache = make_fingerprint_with_content(
            "src/core/cache.rs",
            &["cache_fallback_root"],
            "fn cache_fallback_root() {\n    var();\n    cfg();\n    not();\n    internal_unexpected();\n    join_cache_path();\n}",
        );
        let data = make_fingerprint_with_content(
            "src/core/data.rs",
            &["homeboy_data"],
            "fn homeboy_data() {\n    var();\n    cfg();\n    not();\n    internal_unexpected();\n    join_data_path();\n}",
        );

        // Default config: detector may still flag (this is the FP we are fixing).
        // We do NOT assert a specific shape here — issue #2334 covers a body-shape
        // gate that may also suppress this. We only require that the extension
        // hook genuinely silences it.

        // With extension config matching the rust manifest:
        let cfg = DuplicationDetectorConfig {
            trivial_calls: vec!["var".to_string(), "cfg".to_string(), "not".to_string()],
            plumbing_calls: vec!["internal_unexpected".to_string()],
        };
        let findings = detect_parallel_implementations(
            &[&cache, &data],
            &std::collections::HashSet::new(),
            &cfg,
        );
        assert!(
            findings.is_empty(),
            "Issue #2333: env-derived path helpers should NOT flag once rust extension supplies trivial/plumbing lists, got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn extension_lists_augment_built_in_floor_not_replace() {
        // Built-in floors must remain active even when an extension supplies
        // its own lists. Two methods sharing only built-in trivial calls
        // (`to_string`, `clone`, `unwrap`) should not flag, regardless of
        // extension config contents.
        let fp1 = make_fingerprint_with_content(
            "src/a.rs",
            &["render_a"],
            "fn render_a() {\n    to_string();\n    clone();\n    unwrap();\n    len();\n    iter();\n}",
        );
        let fp2 = make_fingerprint_with_content(
            "src/b.rs",
            &["render_b"],
            "fn render_b() {\n    to_string();\n    clone();\n    unwrap();\n    len();\n    iter();\n}",
        );

        // Extension config that does NOT mention to_string/clone/etc.
        let cfg = DuplicationDetectorConfig {
            trivial_calls: vec!["something_unrelated".to_string()],
            plumbing_calls: vec!["another_unrelated".to_string()],
        };
        let findings =
            detect_parallel_implementations(&[&fp1, &fp2], &std::collections::HashSet::new(), &cfg);
        assert!(
            findings.is_empty(),
            "Built-in trivial floor must remain active even with extension config, got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn corpus_common_calls_filter_broad_boilerplate_signal() {
        // Generic regression for issue #2398: if a call tuple appears across a
        // broad slice of the scanned component, it is scaffolding for this
        // detector. The names are intentionally framework-neutral fixtures;
        // extensions can still provide explicit trivial/plumbing lists.
        let mut fingerprints = Vec::new();

        for idx in 0..8 {
            let method = format!("boilerplate_holder_{idx}");
            let content = format!(
                "fn {method}() {{\n    scaffold_response();\n    read_request();\n    default_payload();\n    validate_presence();\n    filler_{idx}();\n}}"
            );
            fingerprints.push(make_fingerprint_with_content(
                &format!("src/common_{idx}.rs"),
                &[method.as_str()],
                &content,
            ));
        }

        let first = make_fingerprint_with_content(
            "src/a.rs",
            &["create_item"],
            "fn create_item() {\n    scaffold_response();\n    read_request();\n    default_payload();\n    validate_presence();\n    create_specific_step();\n}",
        );
        let second = make_fingerprint_with_content(
            "src/b.rs",
            &["delete_item"],
            "fn delete_item() {\n    scaffold_response();\n    read_request();\n    default_payload();\n    validate_presence();\n    delete_specific_step();\n}",
        );
        fingerprints.push(first);
        fingerprints.push(second);

        let refs = fingerprints.iter().collect::<Vec<_>>();
        let findings = detect_parallel_implementations(
            &refs,
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert!(
            findings.is_empty(),
            "Corpus-common scaffolding calls should not produce a parallel implementation finding, got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }

    #[test]
    fn corpus_common_calls_preserve_domain_specific_signal() {
        let mut fingerprints = Vec::new();

        for idx in 0..8 {
            let method = format!("boilerplate_holder_{idx}");
            let content = format!(
                "fn {method}() {{\n    scaffold_response();\n    read_request();\n    default_payload();\n    validate_presence();\n    filler_{idx}();\n}}"
            );
            fingerprints.push(make_fingerprint_with_content(
                &format!("src/common_{idx}.rs"),
                &[method.as_str()],
                &content,
            ));
        }

        let first = make_fingerprint_with_content(
            "src/deploy.rs",
            &["deploy_item"],
            "fn deploy_item() {\n    scaffold_response();\n    read_request();\n    validate_component();\n    build_artifact();\n    upload_to_host();\n    run_post_hooks();\n    verify_release();\n    deploy_specific_step();\n}",
        );
        let second = make_fingerprint_with_content(
            "src/upgrade.rs",
            &["upgrade_item"],
            "fn upgrade_item() {\n    scaffold_response();\n    read_request();\n    validate_component();\n    build_artifact();\n    upload_to_host();\n    run_post_hooks();\n    verify_release();\n    upgrade_specific_step();\n}",
        );
        fingerprints.push(first);
        fingerprints.push(second);

        let refs = fingerprints.iter().collect::<Vec<_>>();
        let findings = detect_parallel_implementations(
            &refs,
            &std::collections::HashSet::new(),
            &DuplicationDetectorConfig::default(),
        );

        assert_eq!(
            findings.len(),
            2,
            "Domain-specific shared workflow calls should still flag after common scaffolding is discounted"
        );
        assert!(
            findings
                .iter()
                .all(|f| f.description.contains("`build_artifact`")
                    && !f.description.contains("`read_request`")),
            "Findings should be driven by domain calls, got: {:?}",
            findings.iter().map(|f| &f.description).collect::<Vec<_>>()
        );
    }
}
