use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::Path;

use rustc_hash::FxHashSet;

fn relative_key_path(path: &Path, root: &Path) -> String {
    let simple_path = dunce::simplified(path);
    let simple_root = dunce::simplified(root);
    simple_path
        .strip_prefix(simple_root)
        .unwrap_or(simple_path)
        .to_string_lossy()
        .replace('\\', "/")
}

fn dependency_location_key(location: &fallow_core::results::DependencyLocation) -> &'static str {
    match location {
        fallow_core::results::DependencyLocation::Dependencies => "unused-dependency",
        fallow_core::results::DependencyLocation::DevDependencies => "unused-dev-dependency",
        fallow_core::results::DependencyLocation::OptionalDependencies => {
            "unused-optional-dependency"
        }
    }
}

fn unused_dependency_key(item: &fallow_core::results::UnusedDependency, root: &Path) -> String {
    format!(
        "{}:{}:{}",
        dependency_location_key(&item.location),
        relative_key_path(&item.path, root),
        item.package_name
    )
}

fn unlisted_dependency_key(item: &fallow_core::results::UnlistedDependency, root: &Path) -> String {
    let mut sites = item
        .imported_from
        .iter()
        .map(|site| {
            format!(
                "{}:{}:{}",
                relative_key_path(&site.path, root),
                site.line,
                site.col
            )
        })
        .collect::<Vec<_>>();
    sites.sort();
    sites.dedup();
    format!(
        "unlisted-dependency:{}:{}",
        item.package_name,
        sites.join("|")
    )
}

fn unused_member_key(
    rule_id: &str,
    item: &fallow_core::results::UnusedMember,
    root: &Path,
) -> String {
    format!(
        "{}:{}:{}:{}",
        rule_id,
        relative_key_path(&item.path, root),
        item.parent_name,
        item.member_name
    )
}

fn unused_catalog_entry_key(
    item: &fallow_core::results::UnusedCatalogEntry,
    root: &Path,
) -> String {
    format!(
        "unused-catalog-entry:{}:{}:{}:{}",
        relative_key_path(&item.path, root),
        item.line,
        item.catalog_name,
        item.entry_name
    )
}

fn empty_catalog_group_key(item: &fallow_core::results::EmptyCatalogGroup, root: &Path) -> String {
    format!(
        "empty-catalog-group:{}:{}:{}",
        relative_key_path(&item.path, root),
        item.line,
        item.catalog_name
    )
}

fn sorted_relative_path_keys<'a>(
    paths: impl Iterator<Item = &'a Path>,
    root: &Path,
) -> Vec<String> {
    let mut keys = paths
        .map(|path| relative_key_path(path, root))
        .collect::<Vec<_>>();
    keys.sort();
    keys
}

fn duplicate_export_key(
    item: &fallow_core::results::DuplicateExportFinding,
    root: &Path,
) -> String {
    let mut locations = sorted_relative_path_keys(
        item.export.locations.iter().map(|loc| loc.path.as_path()),
        root,
    );
    locations.dedup();
    format!(
        "duplicate-export:{}:{}",
        item.export.export_name,
        locations.join("|")
    )
}

fn circular_dependency_key(
    item: &fallow_core::results::CircularDependencyFinding,
    root: &Path,
) -> String {
    let files = sorted_relative_path_keys(
        item.cycle.files.iter().map(std::path::PathBuf::as_path),
        root,
    );
    format!("circular-dependency:{}", files.join("|"))
}

fn re_export_cycle_key(item: &fallow_core::results::ReExportCycleFinding, root: &Path) -> String {
    let kind = match item.cycle.kind {
        fallow_core::results::ReExportCycleKind::MultiNode => "multi-node",
        fallow_core::results::ReExportCycleKind::SelfLoop => "self-loop",
    };
    let files = sorted_relative_path_keys(
        item.cycle.files.iter().map(std::path::PathBuf::as_path),
        root,
    );
    format!("re-export-cycle:{kind}:{}", files.join("|"))
}

fn boundary_violation_key(
    item: &fallow_core::results::BoundaryViolationFinding,
    root: &Path,
) -> String {
    format!(
        "boundary-violation:{}:{}:{}",
        relative_key_path(&item.violation.from_path, root),
        relative_key_path(&item.violation.to_path, root),
        item.violation.import_specifier
    )
}

fn boundary_coverage_key(
    item: &fallow_core::results::BoundaryCoverageViolationFinding,
    root: &Path,
) -> String {
    format!(
        "boundary-coverage:{}",
        relative_key_path(&item.violation.path, root)
    )
}

fn boundary_call_key(
    item: &fallow_core::results::BoundaryCallViolationFinding,
    root: &Path,
) -> String {
    format!(
        "boundary-call:{}:{}",
        relative_key_path(&item.violation.path, root),
        item.violation.callee
    )
}

fn policy_violation_key(
    item: &fallow_core::results::PolicyViolationFinding,
    root: &Path,
) -> String {
    format!(
        "policy-violation:{}:{}/{}:{}",
        relative_key_path(&item.violation.path, root),
        item.violation.pack,
        item.violation.rule_id,
        item.violation.matched
    )
}

fn stale_suppression_key(item: &fallow_core::results::StaleSuppression, root: &Path) -> String {
    format!(
        "stale-suppression:{}:{}",
        relative_key_path(&item.path, root),
        item.description()
    )
}

fn unresolved_catalog_reference_key(
    item: &fallow_core::results::UnresolvedCatalogReferenceFinding,
    root: &Path,
) -> String {
    format!(
        "unresolved-catalog-reference:{}:{}:{}:{}",
        relative_key_path(&item.reference.path, root),
        item.reference.line,
        item.reference.catalog_name,
        item.reference.entry_name
    )
}

fn unused_dependency_override_key(
    item: &fallow_core::results::UnusedDependencyOverrideFinding,
    root: &Path,
) -> String {
    format!(
        "unused-dependency-override:{}:{}:{}",
        relative_key_path(&item.entry.path, root),
        item.entry.line,
        item.entry.raw_key
    )
}

fn misconfigured_dependency_override_key(
    item: &fallow_core::results::MisconfiguredDependencyOverrideFinding,
    root: &Path,
) -> String {
    format!(
        "misconfigured-dependency-override:{}:{}:{}",
        relative_key_path(&item.entry.path, root),
        item.entry.line,
        item.entry.raw_key
    )
}

/// Build the set of audit attribution keys for all dead-code findings in
/// `results`.
///
/// Each key is a stable string that uniquely identifies one finding across
/// runs (e.g. `unused-file:src/dead.ts`, `unused-export:src/a.ts:Foo`).
/// `retain_introduced_dead_code` and `annotate_dead_code_json` use the same
/// key format to diff the current run against a base snapshot.
///
/// This destructure is deliberately exhaustive: adding a field to
/// `AnalysisResults` must fail compilation here so the author decides
/// explicitly whether the new finding type needs an audit key (add a loop)
/// or has no key representation today (bind with underscore and document why).
///
/// Sibling exhaustive sites: `fallow_core::changed_files::filter_results_by_changed_files`,
/// `dead_code_keys`, `retain_introduced_dead_code`.
/// Non-exhaustive siblings the compiler will NOT flag (wire manually when a
/// finding type is added): `annotate_dead_code_json` (same key formats, this
/// file) and the per-collection severity branches in
/// `crates/cli/src/check/rules.rs` (`apply_rules`, `has_error_severity_issues`).
/// TypeScript mirror: `editors/vscode/scripts/codegen-types.mjs` (`BARE_DEAD_CODE_ALIASES`).
pub(super) fn dead_code_keys(
    results: &fallow_core::results::AnalysisResults,
    root: &Path,
) -> FxHashSet<String> {
    let fallow_core::results::AnalysisResults {
        unused_files,
        unused_exports,
        unused_types,
        private_type_leaks,
        unused_dependencies,
        unused_dev_dependencies,
        unused_optional_dependencies,
        unused_enum_members,
        unused_class_members,
        unresolved_imports,
        unlisted_dependencies,
        duplicate_exports,
        type_only_dependencies,
        test_only_dependencies,
        circular_dependencies,
        re_export_cycles,
        boundary_violations,
        boundary_coverage_violations,
        boundary_call_violations,
        policy_violations,
        stale_suppressions,
        unused_catalog_entries,
        empty_catalog_groups,
        unresolved_catalog_references,
        unused_dependency_overrides,
        misconfigured_dependency_overrides,
        // Non-finding fields: counts and metadata, not attributable to a key.
        suppression_count: _suppression_count,
        active_suppressions: _active_suppressions,
        feature_flags: _feature_flags,
        // Security findings are emitted via `fallow security`, not the audit
        // dead-code gate; they have no dead-code key representation today.
        security_findings: _security_findings,
        security_unresolved_edge_files: _security_unresolved_edge_files,
        security_unresolved_callee_sites: _security_unresolved_callee_sites,
        security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
        // Export usages and entry-point summary are metadata, not issue
        // collections; no key needed.
        export_usages: _export_usages,
        entry_point_summary: _entry_point_summary,
    } = results;

    let mut collector = DeadCodeKeyCollector::new(root);
    collector.add_unused_files(unused_files);
    collector.add_unused_exports(unused_exports);
    collector.add_unused_types(unused_types);
    collector.add_private_type_leaks(private_type_leaks);
    collector.add_unused_dependencies(unused_dependencies);
    collector.add_unused_dev_dependencies(unused_dev_dependencies);
    collector.add_unused_optional_dependencies(unused_optional_dependencies);
    collector.add_unused_enum_members(unused_enum_members);
    collector.add_unused_class_members(unused_class_members);
    collector.add_unresolved_imports(unresolved_imports);
    collector.add_unlisted_dependencies(unlisted_dependencies);
    collector.add_duplicate_exports(duplicate_exports);
    collector.add_type_only_dependencies(type_only_dependencies);
    collector.add_test_only_dependencies(test_only_dependencies);
    collector.add_circular_dependencies(circular_dependencies);
    collector.add_re_export_cycles(re_export_cycles);
    collector.add_boundary_violations(boundary_violations);
    collector.add_boundary_coverage_violations(boundary_coverage_violations);
    collector.add_boundary_call_violations(boundary_call_violations);
    collector.add_policy_violations(policy_violations);
    collector.add_stale_suppressions(stale_suppressions);
    collector.add_unresolved_catalog_references(unresolved_catalog_references);
    collector.add_unused_catalog_entries(unused_catalog_entries);
    collector.add_empty_catalog_groups(empty_catalog_groups);
    collector.add_unused_dependency_overrides(unused_dependency_overrides);
    collector.add_misconfigured_dependency_overrides(misconfigured_dependency_overrides);
    collector.into_keys()
}

struct DeadCodeKeyCollector<'a> {
    root: &'a Path,
    keys: FxHashSet<String>,
}

impl<'a> DeadCodeKeyCollector<'a> {
    fn new(root: &'a Path) -> Self {
        Self {
            root,
            keys: FxHashSet::default(),
        }
    }

    fn into_keys(self) -> FxHashSet<String> {
        self.keys
    }

    fn insert(&mut self, key: String) {
        self.keys.insert(key);
    }

    fn add_unused_files(&mut self, items: &[fallow_core::results::UnusedFileFinding]) {
        for item in items {
            self.insert(format!(
                "unused-file:{}",
                relative_key_path(&item.file.path, self.root)
            ));
        }
    }

    fn add_unused_exports(&mut self, items: &[fallow_core::results::UnusedExportFinding]) {
        for item in items {
            self.insert(format!(
                "unused-export:{}:{}",
                relative_key_path(&item.export.path, self.root),
                item.export.export_name
            ));
        }
    }

    fn add_unused_types(&mut self, items: &[fallow_core::results::UnusedTypeFinding]) {
        for item in items {
            self.insert(format!(
                "unused-type:{}:{}",
                relative_key_path(&item.export.path, self.root),
                item.export.export_name
            ));
        }
    }

    fn add_private_type_leaks(&mut self, items: &[fallow_core::results::PrivateTypeLeakFinding]) {
        for item in items {
            self.insert(format!(
                "private-type-leak:{}:{}:{}",
                relative_key_path(&item.leak.path, self.root),
                item.leak.export_name,
                item.leak.type_name
            ));
        }
    }

    fn add_unused_dependencies(&mut self, items: &[fallow_core::results::UnusedDependencyFinding]) {
        for item in items {
            self.insert(unused_dependency_key(&item.dep, self.root));
        }
    }

    fn add_unused_dev_dependencies(
        &mut self,
        items: &[fallow_core::results::UnusedDevDependencyFinding],
    ) {
        for item in items {
            self.insert(unused_dependency_key(&item.dep, self.root));
        }
    }

    fn add_unused_optional_dependencies(
        &mut self,
        items: &[fallow_core::results::UnusedOptionalDependencyFinding],
    ) {
        for item in items {
            self.insert(unused_dependency_key(&item.dep, self.root));
        }
    }

    fn add_unused_enum_members(&mut self, items: &[fallow_core::results::UnusedEnumMemberFinding]) {
        for item in items {
            self.insert(unused_member_key(
                "unused-enum-member",
                &item.member,
                self.root,
            ));
        }
    }

    fn add_unused_class_members(
        &mut self,
        items: &[fallow_core::results::UnusedClassMemberFinding],
    ) {
        for item in items {
            self.insert(unused_member_key(
                "unused-class-member",
                &item.member,
                self.root,
            ));
        }
    }

    fn add_unresolved_imports(&mut self, items: &[fallow_core::results::UnresolvedImportFinding]) {
        for item in items {
            self.insert(format!(
                "unresolved-import:{}:{}",
                relative_key_path(&item.import.path, self.root),
                item.import.specifier
            ));
        }
    }

    fn add_unlisted_dependencies(
        &mut self,
        items: &[fallow_core::results::UnlistedDependencyFinding],
    ) {
        for item in items {
            self.insert(unlisted_dependency_key(&item.dep, self.root));
        }
    }

    fn add_duplicate_exports(&mut self, items: &[fallow_core::results::DuplicateExportFinding]) {
        for item in items {
            self.insert(duplicate_export_key(item, self.root));
        }
    }

    fn add_type_only_dependencies(
        &mut self,
        items: &[fallow_core::results::TypeOnlyDependencyFinding],
    ) {
        for item in items {
            self.insert(format!(
                "type-only-dependency:{}:{}",
                relative_key_path(&item.dep.path, self.root),
                item.dep.package_name
            ));
        }
    }

    fn add_test_only_dependencies(
        &mut self,
        items: &[fallow_core::results::TestOnlyDependencyFinding],
    ) {
        for item in items {
            self.insert(format!(
                "test-only-dependency:{}:{}",
                relative_key_path(&item.dep.path, self.root),
                item.dep.package_name
            ));
        }
    }

    fn add_circular_dependencies(
        &mut self,
        items: &[fallow_core::results::CircularDependencyFinding],
    ) {
        for item in items {
            self.insert(circular_dependency_key(item, self.root));
        }
    }

    fn add_re_export_cycles(&mut self, items: &[fallow_core::results::ReExportCycleFinding]) {
        for item in items {
            self.insert(re_export_cycle_key(item, self.root));
        }
    }

    fn add_boundary_violations(
        &mut self,
        items: &[fallow_core::results::BoundaryViolationFinding],
    ) {
        for item in items {
            self.insert(boundary_violation_key(item, self.root));
        }
    }

    fn add_boundary_coverage_violations(
        &mut self,
        items: &[fallow_core::results::BoundaryCoverageViolationFinding],
    ) {
        for item in items {
            self.insert(boundary_coverage_key(item, self.root));
        }
    }

    fn add_boundary_call_violations(
        &mut self,
        items: &[fallow_core::results::BoundaryCallViolationFinding],
    ) {
        for item in items {
            self.insert(boundary_call_key(item, self.root));
        }
    }

    fn add_policy_violations(&mut self, items: &[fallow_core::results::PolicyViolationFinding]) {
        for item in items {
            self.insert(policy_violation_key(item, self.root));
        }
    }

    fn add_stale_suppressions(&mut self, items: &[fallow_core::results::StaleSuppression]) {
        for item in items {
            self.insert(stale_suppression_key(item, self.root));
        }
    }

    fn add_unresolved_catalog_references(
        &mut self,
        items: &[fallow_core::results::UnresolvedCatalogReferenceFinding],
    ) {
        for item in items {
            self.insert(unresolved_catalog_reference_key(item, self.root));
        }
    }

    fn add_unused_catalog_entries(
        &mut self,
        items: &[fallow_core::results::UnusedCatalogEntryFinding],
    ) {
        for item in items {
            self.insert(unused_catalog_entry_key(&item.entry, self.root));
        }
    }

    fn add_empty_catalog_groups(
        &mut self,
        items: &[fallow_core::results::EmptyCatalogGroupFinding],
    ) {
        for item in items {
            self.insert(empty_catalog_group_key(&item.group, self.root));
        }
    }

    fn add_unused_dependency_overrides(
        &mut self,
        items: &[fallow_core::results::UnusedDependencyOverrideFinding],
    ) {
        for item in items {
            self.insert(unused_dependency_override_key(item, self.root));
        }
    }

    fn add_misconfigured_dependency_overrides(
        &mut self,
        items: &[fallow_core::results::MisconfiguredDependencyOverrideFinding],
    ) {
        for item in items {
            self.insert(misconfigured_dependency_override_key(item, self.root));
        }
    }
}

/// Retain only findings whose audit key was NOT present in `base` (i.e. was
/// introduced on the current branch).
///
/// When `base` is `None` (no baseline), all findings are kept.
///
/// This destructure is deliberately exhaustive: adding a field to
/// `AnalysisResults` must fail compilation here so the author decides
/// explicitly whether the new finding type needs an introduced-retain (add a
/// retain block) or has no key representation today (bind with underscore and
/// document why).
///
/// Sibling exhaustive sites: `fallow_core::changed_files::filter_results_by_changed_files`,
/// `dead_code_keys`, `retain_introduced_dead_code`.
/// Non-exhaustive siblings the compiler will NOT flag (wire manually when a
/// finding type is added): `annotate_dead_code_json` (same key formats, this
/// file) and the per-collection severity branches in
/// `crates/cli/src/check/rules.rs` (`apply_rules`, `has_error_severity_issues`).
/// TypeScript mirror: `editors/vscode/scripts/codegen-types.mjs` (`BARE_DEAD_CODE_ALIASES`).
pub(super) fn retain_introduced_dead_code(
    results: &mut fallow_core::results::AnalysisResults,
    root: &Path,
    base: Option<&FxHashSet<String>>,
) {
    let Some(base) = base else {
        return;
    };

    // Compute the introduced set before taking any mutable borrows. Note the
    // order differs from the pre-destructure code, which narrowed
    // unused_files/exports/types first and computed keys from the narrowed
    // results. Computing from the un-narrowed results is equivalent: those
    // retains keep exactly the items whose key is NOT in `base`, and the
    // `!base.contains(key)` filter below removes the same base-member keys
    // from the full key set, so `introduced` is identical either way.
    let introduced = introduced_dead_code_keys(results, root, base);
    let keep = |key: String| introduced.contains(&key);

    let fallow_core::results::AnalysisResults {
        unused_files,
        unused_exports,
        unused_types,
        private_type_leaks,
        unused_dependencies,
        unused_dev_dependencies,
        unused_optional_dependencies,
        unused_enum_members,
        unused_class_members,
        unresolved_imports,
        unlisted_dependencies,
        duplicate_exports,
        type_only_dependencies,
        test_only_dependencies,
        circular_dependencies,
        re_export_cycles,
        boundary_violations,
        boundary_coverage_violations,
        boundary_call_violations,
        policy_violations,
        stale_suppressions,
        unused_catalog_entries,
        empty_catalog_groups,
        unresolved_catalog_references,
        unused_dependency_overrides,
        misconfigured_dependency_overrides,
        // Non-finding fields: counts and metadata, not subject to base-keyed
        // filtering.
        suppression_count: _suppression_count,
        active_suppressions: _active_suppressions,
        feature_flags: _feature_flags,
        // Security findings are emitted via `fallow security`, not the audit
        // dead-code gate; they have no key representation and are not filtered
        // here.
        security_findings: _security_findings,
        security_unresolved_edge_files: _security_unresolved_edge_files,
        security_unresolved_callee_sites: _security_unresolved_callee_sites,
        security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
        // Export usages and entry-point summary are metadata, not issue
        // collections; no key needed.
        export_usages: _export_usages,
        entry_point_summary: _entry_point_summary,
    } = results;

    // The three "fast path" retains use a direct base-lookup rather than the
    // introduced set; both predicates are equivalent for these collections
    // (see the `introduced` comment above), so this preserves the original
    // behavior.
    retain_introduced_fast_paths(unused_files, unused_exports, unused_types, root, base);
    private_type_leaks.retain(|item| {
        keep(format!(
            "private-type-leak:{}:{}:{}",
            relative_key_path(&item.leak.path, root),
            item.leak.export_name,
            item.leak.type_name
        ))
    });
    unused_dependencies.retain(|item| keep(unused_dependency_key(&item.dep, root)));
    unused_dev_dependencies.retain(|item| keep(unused_dependency_key(&item.dep, root)));
    unused_optional_dependencies.retain(|item| keep(unused_dependency_key(&item.dep, root)));
    unused_enum_members
        .retain(|item| keep(unused_member_key("unused-enum-member", &item.member, root)));
    unused_class_members
        .retain(|item| keep(unused_member_key("unused-class-member", &item.member, root)));
    unresolved_imports.retain(|item| {
        keep(format!(
            "unresolved-import:{}:{}",
            relative_key_path(&item.import.path, root),
            item.import.specifier
        ))
    });
    unlisted_dependencies.retain(|item| keep(unlisted_dependency_key(&item.dep, root)));
    duplicate_exports.retain(|item| keep(duplicate_export_key(item, root)));
    type_only_dependencies.retain(|item| {
        keep(format!(
            "type-only-dependency:{}:{}",
            relative_key_path(&item.dep.path, root),
            item.dep.package_name
        ))
    });
    test_only_dependencies.retain(|item| {
        keep(format!(
            "test-only-dependency:{}:{}",
            relative_key_path(&item.dep.path, root),
            item.dep.package_name
        ))
    });
    circular_dependencies.retain(|item| keep(circular_dependency_key(item, root)));
    re_export_cycles.retain(|item| keep(re_export_cycle_key(item, root)));
    boundary_violations.retain(|item| keep(boundary_violation_key(item, root)));
    boundary_coverage_violations.retain(|item| keep(boundary_coverage_key(item, root)));
    boundary_call_violations.retain(|item| keep(boundary_call_key(item, root)));
    policy_violations.retain(|item| keep(policy_violation_key(item, root)));
    stale_suppressions.retain(|item| keep(stale_suppression_key(item, root)));
    unresolved_catalog_references.retain(|item| keep(unresolved_catalog_reference_key(item, root)));
    unused_catalog_entries.retain(|item| keep(unused_catalog_entry_key(&item.entry, root)));
    empty_catalog_groups.retain(|item| keep(empty_catalog_group_key(&item.group, root)));
    unused_dependency_overrides.retain(|item| keep(unused_dependency_override_key(item, root)));
    misconfigured_dependency_overrides
        .retain(|item| keep(misconfigured_dependency_override_key(item, root)));
}

fn introduced_dead_code_keys(
    results: &fallow_core::results::AnalysisResults,
    root: &Path,
    base: &FxHashSet<String>,
) -> FxHashSet<String> {
    dead_code_keys(results, root)
        .into_iter()
        .filter(|key| !base.contains(key))
        .collect()
}

fn retain_introduced_fast_paths(
    unused_files: &mut Vec<fallow_core::results::UnusedFileFinding>,
    unused_exports: &mut Vec<fallow_core::results::UnusedExportFinding>,
    unused_types: &mut Vec<fallow_core::results::UnusedTypeFinding>,
    root: &Path,
    base: &FxHashSet<String>,
) {
    unused_files.retain(|item| {
        !base.contains(&format!(
            "unused-file:{}",
            relative_key_path(&item.file.path, root)
        ))
    });
    unused_exports.retain(|item| {
        !base.contains(&format!(
            "unused-export:{}:{}",
            relative_key_path(&item.export.path, root),
            item.export.export_name
        ))
    });
    unused_types.retain(|item| {
        !base.contains(&format!(
            "unused-type:{}:{}",
            relative_key_path(&item.export.path, root),
            item.export.export_name
        ))
    });
}

fn issue_was_introduced(key: &str, base: &FxHashSet<String>) -> bool {
    !base.contains(key)
}

fn annotate_issue_array<I>(json: &mut serde_json::Value, key: &str, introduced: I)
where
    I: IntoIterator<Item = bool>,
{
    let Some(items) = json.get_mut(key).and_then(serde_json::Value::as_array_mut) else {
        return;
    };
    for (item, introduced) in items.iter_mut().zip(introduced) {
        if let serde_json::Value::Object(map) = item {
            map.insert("introduced".to_string(), serde_json::json!(introduced));
        }
    }
}

pub(super) fn annotate_dead_code_json(
    json: &mut serde_json::Value,
    results: &fallow_core::results::AnalysisResults,
    root: &Path,
    base: &FxHashSet<String>,
) {
    let mut annotator = DeadCodeJsonAnnotator {
        json,
        results,
        root,
        base,
    };
    annotator.annotate_file_symbols();
    annotator.annotate_dependencies();
    annotator.annotate_members();
    annotator.annotate_imports_and_exports();
    annotator.annotate_graph();
    annotator.annotate_catalog();
}

struct DeadCodeJsonAnnotator<'a> {
    json: &'a mut serde_json::Value,
    results: &'a fallow_core::results::AnalysisResults,
    root: &'a Path,
    base: &'a FxHashSet<String>,
}

impl DeadCodeJsonAnnotator<'_> {
    fn annotate_file_symbols(&mut self) {
        annotate_issue_array(
            self.json,
            "unused_files",
            self.results.unused_files.iter().map(|item| {
                issue_was_introduced(
                    &format!(
                        "unused-file:{}",
                        relative_key_path(&item.file.path, self.root)
                    ),
                    self.base,
                )
            }),
        );
        annotate_issue_array(
            self.json,
            "unused_exports",
            self.results.unused_exports.iter().map(|item| {
                issue_was_introduced(
                    &format!(
                        "unused-export:{}:{}",
                        relative_key_path(&item.export.path, self.root),
                        item.export.export_name
                    ),
                    self.base,
                )
            }),
        );
        annotate_issue_array(
            self.json,
            "unused_types",
            self.results.unused_types.iter().map(|item| {
                issue_was_introduced(
                    &format!(
                        "unused-type:{}:{}",
                        relative_key_path(&item.export.path, self.root),
                        item.export.export_name
                    ),
                    self.base,
                )
            }),
        );
        annotate_issue_array(
            self.json,
            "private_type_leaks",
            self.results.private_type_leaks.iter().map(|item| {
                issue_was_introduced(
                    &format!(
                        "private-type-leak:{}:{}:{}",
                        relative_key_path(&item.leak.path, self.root),
                        item.leak.export_name,
                        item.leak.type_name
                    ),
                    self.base,
                )
            }),
        );
    }

    fn annotate_dependencies(&mut self) {
        annotate_dependency_json(self.json, self.results, self.root, self.base);
        annotate_issue_array(
            self.json,
            "type_only_dependencies",
            self.results.type_only_dependencies.iter().map(|item| {
                issue_was_introduced(
                    &format!(
                        "type-only-dependency:{}:{}",
                        relative_key_path(&item.dep.path, self.root),
                        item.dep.package_name
                    ),
                    self.base,
                )
            }),
        );
        annotate_issue_array(
            self.json,
            "test_only_dependencies",
            self.results.test_only_dependencies.iter().map(|item| {
                issue_was_introduced(
                    &format!(
                        "test-only-dependency:{}:{}",
                        relative_key_path(&item.dep.path, self.root),
                        item.dep.package_name
                    ),
                    self.base,
                )
            }),
        );
    }

    fn annotate_members(&mut self) {
        annotate_member_json(self.json, self.results, self.root, self.base);
    }

    fn annotate_imports_and_exports(&mut self) {
        annotate_issue_array(
            self.json,
            "unresolved_imports",
            self.results.unresolved_imports.iter().map(|item| {
                issue_was_introduced(
                    &format!(
                        "unresolved-import:{}:{}",
                        relative_key_path(&item.import.path, self.root),
                        item.import.specifier
                    ),
                    self.base,
                )
            }),
        );
        annotate_issue_array(
            self.json,
            "unlisted_dependencies",
            self.results.unlisted_dependencies.iter().map(|item| {
                issue_was_introduced(&unlisted_dependency_key(&item.dep, self.root), self.base)
            }),
        );
        annotate_issue_array(
            self.json,
            "duplicate_exports",
            self.results.duplicate_exports.iter().map(|item| {
                let mut locations: Vec<String> = item
                    .export
                    .locations
                    .iter()
                    .map(|loc| relative_key_path(&loc.path, self.root))
                    .collect();
                locations.sort();
                locations.dedup();
                issue_was_introduced(
                    &format!(
                        "duplicate-export:{}:{}",
                        item.export.export_name,
                        locations.join("|")
                    ),
                    self.base,
                )
            }),
        );
    }

    fn annotate_graph(&mut self) {
        annotate_graph_json(self.json, self.results, self.root, self.base);
    }

    fn annotate_catalog(&mut self) {
        annotate_catalog_json(self.json, self.results, self.root, self.base);
    }
}

fn annotate_dependency_json(
    json: &mut serde_json::Value,
    results: &fallow_core::results::AnalysisResults,
    root: &Path,
    base: &FxHashSet<String>,
) {
    annotate_issue_array(
        json,
        "unused_dependencies",
        results
            .unused_dependencies
            .iter()
            .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
    );
    annotate_issue_array(
        json,
        "unused_dev_dependencies",
        results
            .unused_dev_dependencies
            .iter()
            .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
    );
    annotate_issue_array(
        json,
        "unused_optional_dependencies",
        results
            .unused_optional_dependencies
            .iter()
            .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
    );
}

fn annotate_member_json(
    json: &mut serde_json::Value,
    results: &fallow_core::results::AnalysisResults,
    root: &Path,
    base: &FxHashSet<String>,
) {
    annotate_issue_array(
        json,
        "unused_enum_members",
        results.unused_enum_members.iter().map(|item| {
            issue_was_introduced(
                &unused_member_key("unused-enum-member", &item.member, root),
                base,
            )
        }),
    );
    annotate_issue_array(
        json,
        "unused_class_members",
        results.unused_class_members.iter().map(|item| {
            issue_was_introduced(
                &unused_member_key("unused-class-member", &item.member, root),
                base,
            )
        }),
    );
}

fn annotate_graph_json(
    json: &mut serde_json::Value,
    results: &fallow_core::results::AnalysisResults,
    root: &Path,
    base: &FxHashSet<String>,
) {
    annotate_issue_array(
        json,
        "circular_dependencies",
        results.circular_dependencies.iter().map(|item| {
            let mut files: Vec<String> = item
                .cycle
                .files
                .iter()
                .map(|path| relative_key_path(path, root))
                .collect();
            files.sort();
            issue_was_introduced(&format!("circular-dependency:{}", files.join("|")), base)
        }),
    );
    annotate_issue_array(
        json,
        "re_export_cycles",
        results.re_export_cycles.iter().map(|item| {
            let kind = match item.cycle.kind {
                fallow_core::results::ReExportCycleKind::MultiNode => "multi-node",
                fallow_core::results::ReExportCycleKind::SelfLoop => "self-loop",
            };
            let mut files: Vec<String> = item
                .cycle
                .files
                .iter()
                .map(|path| relative_key_path(path, root))
                .collect();
            files.sort();
            issue_was_introduced(&format!("re-export-cycle:{kind}:{}", files.join("|")), base)
        }),
    );
    annotate_issue_array(
        json,
        "boundary_violations",
        results.boundary_violations.iter().map(|item| {
            issue_was_introduced(
                &format!(
                    "boundary-violation:{}:{}:{}",
                    relative_key_path(&item.violation.from_path, root),
                    relative_key_path(&item.violation.to_path, root),
                    item.violation.import_specifier
                ),
                base,
            )
        }),
    );
    annotate_issue_array(
        json,
        "boundary_coverage_violations",
        results.boundary_coverage_violations.iter().map(|item| {
            issue_was_introduced(
                &format!(
                    "boundary-coverage:{}",
                    relative_key_path(&item.violation.path, root)
                ),
                base,
            )
        }),
    );
    annotate_issue_array(
        json,
        "boundary_call_violations",
        results.boundary_call_violations.iter().map(|item| {
            issue_was_introduced(
                &format!(
                    "boundary-call:{}:{}",
                    relative_key_path(&item.violation.path, root),
                    item.violation.callee
                ),
                base,
            )
        }),
    );
    annotate_issue_array(
        json,
        "policy_violations",
        results.policy_violations.iter().map(|item| {
            issue_was_introduced(
                &format!(
                    "policy-violation:{}:{}/{}:{}",
                    relative_key_path(&item.violation.path, root),
                    item.violation.pack,
                    item.violation.rule_id,
                    item.violation.matched
                ),
                base,
            )
        }),
    );
    annotate_issue_array(
        json,
        "stale_suppressions",
        results.stale_suppressions.iter().map(|item| {
            issue_was_introduced(
                &format!(
                    "stale-suppression:{}:{}",
                    relative_key_path(&item.path, root),
                    item.description()
                ),
                base,
            )
        }),
    );
}

fn annotate_catalog_json(
    json: &mut serde_json::Value,
    results: &fallow_core::results::AnalysisResults,
    root: &Path,
    base: &FxHashSet<String>,
) {
    annotate_issue_array(
        json,
        "unresolved_catalog_references",
        results.unresolved_catalog_references.iter().map(|item| {
            issue_was_introduced(
                &format!(
                    "unresolved-catalog-reference:{}:{}:{}:{}",
                    relative_key_path(&item.reference.path, root),
                    item.reference.line,
                    item.reference.catalog_name,
                    item.reference.entry_name
                ),
                base,
            )
        }),
    );
    annotate_issue_array(
        json,
        "unused_catalog_entries",
        results
            .unused_catalog_entries
            .iter()
            .map(|item| issue_was_introduced(&unused_catalog_entry_key(&item.entry, root), base)),
    );
    annotate_issue_array(
        json,
        "empty_catalog_groups",
        results
            .empty_catalog_groups
            .iter()
            .map(|item| issue_was_introduced(&empty_catalog_group_key(&item.group, root), base)),
    );
    annotate_issue_array(
        json,
        "unused_dependency_overrides",
        results.unused_dependency_overrides.iter().map(|item| {
            issue_was_introduced(
                &format!(
                    "unused-dependency-override:{}:{}:{}",
                    relative_key_path(&item.entry.path, root),
                    item.entry.line,
                    item.entry.raw_key
                ),
                base,
            )
        }),
    );
    annotate_issue_array(
        json,
        "misconfigured_dependency_overrides",
        results
            .misconfigured_dependency_overrides
            .iter()
            .map(|item| {
                issue_was_introduced(
                    &format!(
                        "misconfigured-dependency-override:{}:{}:{}",
                        relative_key_path(&item.entry.path, root),
                        item.entry.line,
                        item.entry.raw_key
                    ),
                    base,
                )
            }),
    );
}

pub(super) fn annotate_health_json(
    json: &mut serde_json::Value,
    report: &crate::health_types::HealthReport,
    root: &Path,
    base: &FxHashSet<String>,
) {
    let Some(items) = json
        .get_mut("findings")
        .and_then(serde_json::Value::as_array_mut)
    else {
        return;
    };
    for (item, finding) in items.iter_mut().zip(&report.findings) {
        if let serde_json::Value::Object(map) = item {
            map.insert(
                "introduced".to_string(),
                serde_json::json!(issue_was_introduced(
                    &health_finding_key(finding, root),
                    base
                )),
            );
        }
    }
}

pub(super) fn annotate_dupes_json(
    json: &mut serde_json::Value,
    report: &fallow_core::duplicates::DuplicationReport,
    root: &Path,
    base: &FxHashSet<String>,
) {
    let Some(items) = json
        .get_mut("clone_groups")
        .and_then(serde_json::Value::as_array_mut)
    else {
        return;
    };
    for (item, group) in items.iter_mut().zip(&report.clone_groups) {
        if let serde_json::Value::Object(map) = item {
            map.insert(
                "introduced".to_string(),
                serde_json::json!(issue_was_introduced(&dupe_group_key(group, root), base)),
            );
        }
    }
}

pub(super) fn health_keys(
    report: &crate::health_types::HealthReport,
    root: &Path,
) -> FxHashSet<String> {
    report
        .findings
        .iter()
        .map(|finding| health_finding_key(finding, root))
        .collect()
}

pub(super) fn health_finding_key(
    finding: &crate::health_types::ComplexityViolation,
    root: &Path,
) -> String {
    format!(
        "complexity:{}:{}:{:?}",
        relative_key_path(&finding.path, root),
        finding.name,
        finding.exceeded
    )
}

pub(super) fn dupes_keys(
    report: &fallow_core::duplicates::DuplicationReport,
    root: &Path,
) -> FxHashSet<String> {
    report
        .clone_groups
        .iter()
        .map(|group| dupe_group_key(group, root))
        .collect()
}

pub(super) fn dupe_group_key(group: &fallow_core::duplicates::CloneGroup, root: &Path) -> String {
    let mut files: Vec<String> = group
        .instances
        .iter()
        .map(|instance| relative_key_path(&instance.file, root))
        .collect();
    files.sort();
    files.dedup();
    let mut hasher = DefaultHasher::new();
    for instance in &group.instances {
        instance.fragment.hash(&mut hasher);
    }
    format!(
        "dupe:{}:{}:{}:{:x}",
        files.join("|"),
        group.token_count,
        group.line_count,
        hasher.finish()
    )
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use fallow_core::extract::MemberKind;
    use fallow_core::results::{
        AnalysisResults, BoundaryCallViolation, BoundaryCallViolationFinding,
        BoundaryCoverageViolation, BoundaryCoverageViolationFinding, BoundaryViolation,
        BoundaryViolationFinding, CircularDependency, CircularDependencyFinding,
        DependencyLocation, DependencyOverrideMisconfigReason, DependencyOverrideSource,
        DuplicateExport, DuplicateExportFinding, DuplicateLocation, EmptyCatalogGroup,
        EmptyCatalogGroupFinding, ImportSite, MisconfiguredDependencyOverride,
        MisconfiguredDependencyOverrideFinding, PrivateTypeLeak, PrivateTypeLeakFinding,
        ReExportCycle, ReExportCycleFinding, ReExportCycleKind, StaleSuppression,
        SuppressionOrigin, TestOnlyDependency, TestOnlyDependencyFinding, TypeOnlyDependency,
        TypeOnlyDependencyFinding, UnlistedDependency, UnlistedDependencyFinding,
        UnresolvedCatalogReference, UnresolvedCatalogReferenceFinding, UnresolvedImport,
        UnresolvedImportFinding, UnusedCatalogEntry, UnusedCatalogEntryFinding,
        UnusedClassMemberFinding, UnusedDependency, UnusedDependencyFinding,
        UnusedDependencyOverride, UnusedDependencyOverrideFinding, UnusedDevDependencyFinding,
        UnusedEnumMemberFinding, UnusedExport, UnusedExportFinding, UnusedFile, UnusedFileFinding,
        UnusedMember, UnusedOptionalDependencyFinding, UnusedTypeFinding,
    };
    use rustc_hash::FxHashSet;
    use serde_json::json;

    use super::{
        annotate_dead_code_json, dead_code_keys, relative_key_path, retain_introduced_dead_code,
    };

    fn root() -> PathBuf {
        PathBuf::from("/repo")
    }

    fn export(path: &Path, name: &str) -> UnusedExportFinding {
        UnusedExportFinding::with_actions(UnusedExport {
            path: path.to_path_buf(),
            export_name: name.to_string(),
            is_type_only: false,
            line: 1,
            col: 0,
            span_start: 0,
            is_re_export: false,
        })
    }

    fn unused_file(path: &Path) -> UnusedFileFinding {
        UnusedFileFinding::with_actions(UnusedFile {
            path: path.to_path_buf(),
        })
    }

    fn dependency(path: &Path, package_name: &str) -> UnusedDependencyFinding {
        UnusedDependencyFinding::with_actions(UnusedDependency {
            package_name: package_name.to_string(),
            location: DependencyLocation::Dependencies,
            path: path.to_path_buf(),
            line: 4,
            used_in_workspaces: Vec::new(),
        })
    }

    fn unresolved(path: &Path, specifier: &str) -> UnresolvedImportFinding {
        UnresolvedImportFinding::with_actions(UnresolvedImport {
            path: path.to_path_buf(),
            specifier: specifier.to_string(),
            line: 2,
            col: 1,
            specifier_col: 8,
        })
    }

    fn unlisted(path: &Path, package_name: &str) -> UnlistedDependencyFinding {
        UnlistedDependencyFinding::with_actions(UnlistedDependency {
            package_name: package_name.to_string(),
            imported_from: vec![
                ImportSite {
                    path: path.to_path_buf(),
                    line: 9,
                    col: 2,
                },
                ImportSite {
                    path: path.to_path_buf(),
                    line: 9,
                    col: 2,
                },
            ],
        })
    }

    fn duplicate_export(root: &Path) -> DuplicateExportFinding {
        DuplicateExportFinding::with_actions(DuplicateExport {
            export_name: "Button".to_string(),
            locations: vec![
                DuplicateLocation {
                    path: root.join("src/b.ts"),
                    line: 1,
                    col: 0,
                },
                DuplicateLocation {
                    path: root.join("src/a.ts"),
                    line: 1,
                    col: 0,
                },
                DuplicateLocation {
                    path: root.join("src/a.ts"),
                    line: 2,
                    col: 0,
                },
            ],
        })
    }

    fn sample_results(root: &Path) -> AnalysisResults {
        let source = root.join("src/page.ts");
        let package_json = root.join("package.json");
        let mut results = AnalysisResults::default();
        results
            .unused_files
            .push(unused_file(&root.join("src/dead.ts")));
        results.unused_exports.push(export(&source, "loader"));
        results
            .unused_dependencies
            .push(dependency(&package_json, "left-pad"));
        results
            .unresolved_imports
            .push(unresolved(&source, "./missing"));
        results.unlisted_dependencies.push(unlisted(&source, "zod"));
        results.duplicate_exports.push(duplicate_export(root));
        results
    }

    #[test]
    fn relative_key_path_strips_root_and_normalizes_separators() {
        let path = Path::new("/repo/src\\feature\\index.ts");
        assert_eq!(
            relative_key_path(path, Path::new("/repo")),
            "src/feature/index.ts"
        );
    }

    #[test]
    fn dead_code_keys_are_stable_for_unsorted_and_duplicate_locations() {
        let root = root();
        let keys = dead_code_keys(&sample_results(&root), &root);

        assert!(keys.contains("unused-file:src/dead.ts"));
        assert!(keys.contains("unused-export:src/page.ts:loader"));
        assert!(keys.contains("unused-dependency:package.json:left-pad"));
        assert!(keys.contains("unresolved-import:src/page.ts:./missing"));
        assert!(keys.contains("unlisted-dependency:zod:src/page.ts:9:2"));
        assert!(keys.contains("duplicate-export:Button:src/a.ts|src/b.ts"));
    }

    #[test]
    fn dead_code_keys_cover_type_member_and_dependency_variants() {
        let root = root();
        let source = root.join("src/types.ts");
        let package_json = root.join("package.json");
        let mut results = AnalysisResults::default();
        results
            .unused_types
            .push(UnusedTypeFinding::with_actions(UnusedExport {
                path: source.clone(),
                export_name: "UnusedType".to_string(),
                is_type_only: true,
                line: 3,
                col: 0,
                span_start: 12,
                is_re_export: false,
            }));
        results
            .private_type_leaks
            .push(PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
                path: source.clone(),
                export_name: "makePublic".to_string(),
                type_name: "PrivateShape".to_string(),
                line: 7,
                col: 12,
                span_start: 64,
            }));
        results
            .unused_dev_dependencies
            .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
                package_name: "vite".to_string(),
                location: DependencyLocation::DevDependencies,
                path: package_json.clone(),
                line: 10,
                used_in_workspaces: Vec::new(),
            }));
        results
            .unused_optional_dependencies
            .push(UnusedOptionalDependencyFinding::with_actions(
                UnusedDependency {
                    package_name: "fsevents".to_string(),
                    location: DependencyLocation::OptionalDependencies,
                    path: package_json.clone(),
                    line: 11,
                    used_in_workspaces: Vec::new(),
                },
            ));
        results
            .unused_enum_members
            .push(UnusedEnumMemberFinding::with_actions(UnusedMember {
                path: source.clone(),
                parent_name: "Status".to_string(),
                member_name: "Idle".to_string(),
                kind: MemberKind::EnumMember,
                line: 15,
                col: 2,
            }));
        results
            .unused_class_members
            .push(UnusedClassMemberFinding::with_actions(UnusedMember {
                path: source,
                parent_name: "Controller".to_string(),
                member_name: "legacy".to_string(),
                kind: MemberKind::ClassMethod,
                line: 21,
                col: 2,
            }));
        results
            .type_only_dependencies
            .push(TypeOnlyDependencyFinding::with_actions(
                TypeOnlyDependency {
                    package_name: "zod".to_string(),
                    path: package_json.clone(),
                    line: 12,
                },
            ));
        results
            .test_only_dependencies
            .push(TestOnlyDependencyFinding::with_actions(
                TestOnlyDependency {
                    package_name: "vitest".to_string(),
                    path: package_json,
                    line: 13,
                },
            ));

        let keys = dead_code_keys(&results, &root);

        assert!(keys.contains("unused-type:src/types.ts:UnusedType"));
        assert!(keys.contains("private-type-leak:src/types.ts:makePublic:PrivateShape"));
        assert!(keys.contains("unused-dev-dependency:package.json:vite"));
        assert!(keys.contains("unused-optional-dependency:package.json:fsevents"));
        assert!(keys.contains("unused-enum-member:src/types.ts:Status:Idle"));
        assert!(keys.contains("unused-class-member:src/types.ts:Controller:legacy"));
        assert!(keys.contains("type-only-dependency:package.json:zod"));
        assert!(keys.contains("test-only-dependency:package.json:vitest"));
    }

    #[test]
    fn dead_code_keys_cover_graph_boundary_catalog_and_override_variants() {
        let root = root();
        let source = root.join("src/app.ts");
        let other = root.join("src/other.ts");
        let workspace = root.join("pnpm-workspace.yaml");
        let mut results = AnalysisResults::default();
        results
            .circular_dependencies
            .push(CircularDependencyFinding::with_actions(
                CircularDependency {
                    files: vec![other.clone(), source.clone()],
                    length: 2,
                    line: 4,
                    col: 0,
                    edges: Vec::new(),
                    is_cross_package: false,
                },
            ));
        results
            .re_export_cycles
            .push(ReExportCycleFinding::with_actions(ReExportCycle {
                files: vec![source.clone()],
                kind: ReExportCycleKind::SelfLoop,
            }));
        results
            .boundary_violations
            .push(BoundaryViolationFinding::with_actions(BoundaryViolation {
                from_path: source.clone(),
                to_path: other,
                from_zone: "ui".to_string(),
                to_zone: "server".to_string(),
                import_specifier: "../other".to_string(),
                line: 1,
                col: 0,
            }));
        results
            .boundary_coverage_violations
            .push(BoundaryCoverageViolationFinding::with_actions(
                BoundaryCoverageViolation {
                    path: root.join("src/unmatched.ts"),
                    line: 1,
                    col: 0,
                },
            ));
        results
            .boundary_call_violations
            .push(BoundaryCallViolationFinding::with_actions(
                BoundaryCallViolation {
                    path: source.clone(),
                    line: 12,
                    col: 4,
                    zone: "ui".to_string(),
                    callee: "child_process.exec".to_string(),
                    pattern: "child_process.*".to_string(),
                },
            ));
        results.stale_suppressions.push(StaleSuppression {
            path: source,
            line: 2,
            col: 0,
            origin: SuppressionOrigin::Comment {
                issue_kind: Some("unused-export".to_string()),
                is_file_level: false,
                kind_known: true,
            },
        });
        results.unresolved_catalog_references.push(
            UnresolvedCatalogReferenceFinding::with_actions(UnresolvedCatalogReference {
                entry_name: "react".to_string(),
                catalog_name: "default".to_string(),
                path: root.join("packages/app/package.json"),
                line: 9,
                available_in_catalogs: vec!["react18".to_string()],
            }),
        );
        results
            .unused_catalog_entries
            .push(UnusedCatalogEntryFinding::with_actions(
                UnusedCatalogEntry {
                    entry_name: "lodash".to_string(),
                    catalog_name: "default".to_string(),
                    path: workspace.clone(),
                    line: 3,
                    hardcoded_consumers: Vec::new(),
                },
            ));
        results
            .empty_catalog_groups
            .push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
                catalog_name: "react17".to_string(),
                path: workspace.clone(),
                line: 7,
            }));
        results
            .unused_dependency_overrides
            .push(UnusedDependencyOverrideFinding::with_actions(
                UnusedDependencyOverride {
                    raw_key: "left-pad".to_string(),
                    target_package: "left-pad".to_string(),
                    parent_package: None,
                    version_constraint: None,
                    version_range: "^1.3.0".to_string(),
                    source: DependencyOverrideSource::PnpmWorkspaceYaml,
                    path: workspace.clone(),
                    line: 11,
                    hint: None,
                },
            ));
        results.misconfigured_dependency_overrides.push(
            MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
                raw_key: ">".to_string(),
                target_package: None,
                raw_value: String::new(),
                reason: DependencyOverrideMisconfigReason::UnparsableKey,
                source: DependencyOverrideSource::PnpmWorkspaceYaml,
                path: workspace,
                line: 12,
            }),
        );

        let keys = dead_code_keys(&results, &root);

        assert!(keys.contains("circular-dependency:src/app.ts|src/other.ts"));
        assert!(keys.contains("re-export-cycle:self-loop:src/app.ts"));
        assert!(keys.contains("boundary-violation:src/app.ts:src/other.ts:../other"));
        assert!(keys.contains("boundary-coverage:src/unmatched.ts"));
        assert!(keys.contains("boundary-call:src/app.ts:child_process.exec"));
        assert!(
            keys.contains("stale-suppression:src/app.ts:// fallow-ignore-next-line unused-export")
        );
        assert!(
            keys.contains("unresolved-catalog-reference:packages/app/package.json:9:default:react")
        );
        assert!(keys.contains("unused-catalog-entry:pnpm-workspace.yaml:3:default:lodash"));
        assert!(keys.contains("empty-catalog-group:pnpm-workspace.yaml:7:react17"));
        assert!(keys.contains("unused-dependency-override:pnpm-workspace.yaml:11:left-pad"));
        assert!(keys.contains("misconfigured-dependency-override:pnpm-workspace.yaml:12:>"));
    }

    #[test]
    fn retain_introduced_dead_code_keeps_only_findings_absent_from_base() {
        let root = root();
        let mut results = sample_results(&root);
        let base = FxHashSet::from_iter([
            "unused-file:src/dead.ts".to_string(),
            "unused-dependency:package.json:left-pad".to_string(),
            "unresolved-import:src/page.ts:./missing".to_string(),
        ]);

        retain_introduced_dead_code(&mut results, &root, Some(&base));

        assert!(results.unused_files.is_empty());
        assert!(results.unused_dependencies.is_empty());
        assert!(results.unresolved_imports.is_empty());
        assert_eq!(results.unused_exports.len(), 1);
        assert_eq!(results.unlisted_dependencies.len(), 1);
        assert_eq!(results.duplicate_exports.len(), 1);
    }

    #[test]
    fn annotate_dead_code_json_marks_introduced_status_by_matching_key_order() {
        let root = root();
        let results = sample_results(&root);
        let base = FxHashSet::from_iter([
            "unused-file:src/dead.ts".to_string(),
            "unlisted-dependency:zod:src/page.ts:9:2".to_string(),
        ]);
        let mut json = json!({
            "unused_files": [{}],
            "unused_exports": [{}],
            "unused_dependencies": [{}],
            "unresolved_imports": [{}],
            "unlisted_dependencies": [{}],
            "duplicate_exports": [{}],
        });

        annotate_dead_code_json(&mut json, &results, &root, &base);

        assert_eq!(json["unused_files"][0]["introduced"], false);
        assert_eq!(json["unused_exports"][0]["introduced"], true);
        assert_eq!(json["unused_dependencies"][0]["introduced"], true);
        assert_eq!(json["unresolved_imports"][0]["introduced"], true);
        assert_eq!(json["unlisted_dependencies"][0]["introduced"], false);
        assert_eq!(json["duplicate_exports"][0]["introduced"], true);
    }
}
